mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fix(web): unify dashboard pagination + bump fleet-health page size
Dashboard: removed the standalone "Load 100 more sessions..." button. The
session table now has a single Prev/Next surface that transparently fetches
the next server batch when the user pages past locally-loaded sessions —
the existing auto-load effect in page.tsx already drives loadMore() once
currentPage * sessionsPerPage exceeds sessions.length, but handleNextPage
was clamping to totalPages so currentPage could never overshoot, leaving
"Next" disabled until the user clicked the separate Load-More button.
- useDashboardFilters now takes hasMore + loadingMore; handleNextPage
advances past totalPages when hasMore && !loadingMore. A snap-back
effect resets currentPage to totalPages once a load finishes that
didn't grow totalPages (filter case: new batch yielded zero matches).
- SessionTable renders the Prev/Next controls whenever there's somewhere
to navigate (totalPages > 1 || hasMore), shows "Page X of Y+" while
more is on the server, disables Next only at the true end, and shows
an inline spinner inside Next during loadingMore. The empty tbody
distinguishes "loading sessions..." from "no results".
Fleet Health: bumped FLEET_PAGE_SIZE from 200 to 1000 (backend MaxPageSize
is 1000). A 90-day window on a busy install now drains in ~1/5 the
round-trips, eliminating the 5x roundtrip-amplification users observed
on initial load.
Type-check clean, vitest 249 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,6 @@ interface SessionTableProps {
|
||||
onSessionsPerPageChange: (value: number) => void;
|
||||
hasMore: boolean;
|
||||
loadingMore: boolean;
|
||||
onLoadMore: () => void;
|
||||
onLoadAll: () => void;
|
||||
adminMode: boolean;
|
||||
globalAdminMode: boolean;
|
||||
@@ -109,7 +108,6 @@ export function SessionTable({
|
||||
onSessionsPerPageChange,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
onLoadMore,
|
||||
onLoadAll,
|
||||
adminMode,
|
||||
globalAdminMode,
|
||||
@@ -722,7 +720,17 @@ export function SessionTable({
|
||||
{paginatedSessions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={colSpan} className="px-6 py-8 text-center text-gray-500">
|
||||
No sessions found matching your search.
|
||||
{loadingMore ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Loading sessions...
|
||||
</span>
|
||||
) : (
|
||||
"No sessions found matching your search."
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
@@ -752,75 +760,39 @@ export function SessionTable({
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
{/* Pagination Controls — single Prev/Next surface that transparently fetches
|
||||
the next server batch when the user pages past locally-loaded sessions.
|
||||
Renders whenever there's somewhere to navigate (more local pages OR more
|
||||
on the server). */}
|
||||
{(totalPages > 1 || hasMore) && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-700">
|
||||
Page {currentPage} of {totalPages} ({sortedSessions.length}{hasMore ? '+' : ''} total sessions)
|
||||
Page {currentPage} of {totalPages}{hasMore ? '+' : ''} ({sortedSessions.length}{hasMore ? '+' : ''} total sessions)
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onPreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
disabled={currentPage === 1 || loadingMore}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={onNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={(currentPage >= totalPages && !hasMore) || loadingMore}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2"
|
||||
>
|
||||
{loadingMore && (
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
)}
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load More Button */}
|
||||
{hasMore && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
{searchQuery.trim() ? (
|
||||
// Search active: offer to load all remaining sessions
|
||||
<button
|
||||
onClick={onLoadAll}
|
||||
disabled={loadingMore}
|
||||
className="px-6 py-2 bg-gray-100 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loadingMore ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Searching... ({sessions.length} sessions loaded)
|
||||
</span>
|
||||
) : (
|
||||
'Search all sessions...'
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
// No search: standard load-more (100 at a time)
|
||||
<button
|
||||
onClick={onLoadMore}
|
||||
disabled={loadingMore}
|
||||
className="px-6 py-2 bg-gray-100 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loadingMore ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Loading...
|
||||
</span>
|
||||
) : (
|
||||
'Load 100 more sessions...'
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ interface UseDashboardFiltersParams {
|
||||
tenantId: string | null | undefined;
|
||||
globalAdminMode: boolean;
|
||||
tenantIdFilter: string;
|
||||
hasMore: boolean;
|
||||
loadingMore: boolean;
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
@@ -57,6 +59,8 @@ export function useDashboardFilters({
|
||||
tenantId,
|
||||
globalAdminMode,
|
||||
tenantIdFilter,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
}: UseDashboardFiltersParams): UseDashboardFiltersReturn {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||
@@ -213,7 +217,24 @@ export function useDashboardFilters({
|
||||
}, [sortedSessions, currentPage, sessionsPerPage]);
|
||||
|
||||
const handlePreviousPage = () => setCurrentPage((prev) => Math.max(1, prev - 1));
|
||||
const handleNextPage = () => setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
||||
// Allow advancing past totalPages when the server has more pages; the auto-load
|
||||
// effect in page.tsx will fetch the next batch and totalPages catches up. Without
|
||||
// this overshoot the user could never trigger a load while staying inside a single
|
||||
// Prev/Next control — they would have to click a separate "Load more" button.
|
||||
const handleNextPage = () => setCurrentPage((prev) => {
|
||||
if (prev < totalPages) return prev + 1;
|
||||
if (hasMore && !loadingMore) return prev + 1;
|
||||
return prev;
|
||||
});
|
||||
|
||||
// Snap currentPage back to the last valid page once a load finishes that did NOT
|
||||
// grow totalPages (e.g. an active filter narrowed the new batch to zero matches).
|
||||
// Without this the user would be stuck on an empty paginated slice with no way
|
||||
// to recover except clicking Prev.
|
||||
useEffect(() => {
|
||||
if (loadingMore) return;
|
||||
if (totalPages > 0 && currentPage > totalPages) setCurrentPage(totalPages);
|
||||
}, [currentPage, totalPages, loadingMore]);
|
||||
|
||||
return {
|
||||
searchQuery, setSearchQuery,
|
||||
|
||||
@@ -66,6 +66,8 @@ export default function Home() {
|
||||
tenantId,
|
||||
globalAdminMode,
|
||||
tenantIdFilter,
|
||||
hasMore,
|
||||
loadingMore,
|
||||
});
|
||||
|
||||
// Redirect regular users (non-admin, non-operator) to progress portal – they must never see the session list
|
||||
@@ -275,7 +277,6 @@ export default function Home() {
|
||||
onSessionsPerPageChange={handleSessionsPerPageChange}
|
||||
hasMore={hasMore}
|
||||
loadingMore={loadingMore}
|
||||
onLoadMore={loadMore}
|
||||
onLoadAll={loadAll}
|
||||
adminMode={adminMode}
|
||||
globalAdminMode={globalAdminMode}
|
||||
|
||||
@@ -14,7 +14,9 @@ import FleetStatCard from "./components/FleetStatCard";
|
||||
import { Session } from "@/types";
|
||||
import { useAdminMode } from "@/hooks/useAdminMode";
|
||||
|
||||
const FLEET_PAGE_SIZE = 200;
|
||||
// Backend caps pageSize at 1000 — use the maximum so a 90-day window on a busy
|
||||
// install (10k+ sessions) drains in a handful of round-trips instead of 50+.
|
||||
const FLEET_PAGE_SIZE = 1000;
|
||||
|
||||
interface AppMetric {
|
||||
appName: string;
|
||||
|
||||
Reference in New Issue
Block a user