routes: keep paging when client-side region filtering starves the table

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.
This commit is contained in:
MrAlders0n
2026-06-10 07:03:16 -04:00
parent d5deb1e042
commit a78b735bb2
2 changed files with 32 additions and 3 deletions
+11 -1
View File
@@ -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<KnownRoute>({
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(() => {
+21 -2
View File
@@ -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 }) => (
<QueryClientProvider client={client}>
<RegionProvider defaultSelection={ALL_REGIONS}>{children}</RegionProvider>
<RegionProvider defaultSelection={selection}>{children}</RegionProvider>
</QueryClientProvider>
);
render(<RouteTable />, { 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,