From a78b735bb2e8657a66be40f99dc3e2bb8ecb5d15 Mon Sep 17 00:00:00 2001 From: MrAlders0n Date: Wed, 10 Jun 2026 07:03:16 -0400 Subject: [PATCH] routes: keep paging when client-side region filtering starves the table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multi-IATA region filters globally-paged rows client-side, and scroll was the only trigger for the next page — an empty or too-short filtered list could never fire it, so routes deeper in the cursor stream were unreachable ('No routes' with routes that exist). Pull pages until the region has a screenful or the cap calls it sparse. --- src/features/routes/RouteTable.tsx | 12 +++++++++++- tests/features/routes/RouteTable.test.tsx | 23 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/features/routes/RouteTable.tsx b/src/features/routes/RouteTable.tsx index 2282c2f..a92b051 100644 --- a/src/features/routes/RouteTable.tsx +++ b/src/features/routes/RouteTable.tsx @@ -169,7 +169,7 @@ export function RouteTable() { // Page the route set on demand (50 at a time, cursor = last route's lastSeen ms) — the DataTable // pulls the next page via loadMore() as you scroll, instead of eagerly loading the whole set. - const { items: listRoutes, loadedCount, isPaging, isError, isLoading: listLoading, loadMore } = + const { items: listRoutes, loadedCount, isPaging, isError, isLoading: listLoading, loadMore, hasMore } = useInfinitePages({ queryKey: ["routes", serverIata ?? ""], queryFn: (cursor) => getKnownRoutesPage({ iata: serverIata, cursor }), @@ -223,6 +223,16 @@ export function RouteTable() { [rows, selectedKey], ); + // A multi-IATA region filters globally-paged rows client-side, so the filtered list can be too + // short to ever trigger scroll paging — or empty, with the region's routes deeper in the cursor + // stream. Keep pulling pages until there's a screenful or the cap says the region is just sparse. + useEffect(() => { + if (search || serverIata || !iatas?.length) return; + if (!hasMore || isPaging) return; + if ((rows?.length ?? 0) >= 50 || loadedCount >= 1000) return; + loadMore(); + }, [search, serverIata, iatas, hasMore, isPaging, rows, loadedCount, loadMore]); + const canSearch = !!(from.trim() && to.trim() && searchIatas.length >= 1); // clear any selection when the visible list changes out from under it (search submit/clear) const submitSearch = useCallback(() => { diff --git a/tests/features/routes/RouteTable.test.tsx b/tests/features/routes/RouteTable.test.tsx index 2898214..1a3fd64 100644 --- a/tests/features/routes/RouteTable.test.tsx +++ b/tests/features/routes/RouteTable.test.tsx @@ -30,11 +30,11 @@ const mockGetRegions = vi.mocked(getRegions); const node = (id: string, name: string) => ({ id, name, publicKey: "deadbeef" }); -function renderTable() { +function renderTable(selection = ALL_REGIONS) { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const wrapper = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); render(, { wrapper }); @@ -103,6 +103,25 @@ describe("RouteTable search", () => { expect(screen.getByText("Dst Node")).toBeInTheDocument(); }); + it("keeps paging for a multi-IATA region until its routes surface", async () => { + const foreign: KnownRoute = { id: 1, iata: "CCC", hopCount: 1, hops: [], firstSeen: 1, lastSeen: 5, observationCount: 9 }; + const wanted: KnownRoute = { id: 2, iata: "AAA", hopCount: 2, hops: [], firstSeen: 1, lastSeen: 3, observationCount: 17 }; + // first global page has nothing from the region; the region's route sits on page two + mockGetKnownRoutesPage.mockImplementation(({ cursor } = {}) => + Promise.resolve( + cursor === undefined + ? { items: [foreign], nextCursor: 5, hasMore: true } + : { items: [wanted], nextCursor: null, hasMore: false }, + ), + ); + + renderTable({ regions: [], iatas: ["AAA", "BBB"] }); + + // without fill-paging the table dead-ends on "No routes" — scroll can never fire on an empty list + expect(await screen.findByText("17")).toBeInTheDocument(); + expect(mockGetKnownRoutesPage.mock.calls.length).toBeGreaterThanOrEqual(2); + }); + it("shows a route's observation count in the list", async () => { const route: KnownRoute = { id: 7,