From 002fb1bccba5985e1b20f74b112139ddf0195878 Mon Sep 17 00:00:00 2001 From: Juan Denis Date: Wed, 12 Aug 2026 17:38:58 -0400 Subject: [PATCH] fix(views): the view bar counted rows the table was not showing Every page built its own "N of M" from its OWN array, which is only search-filtered -- the column rules are applied inside DataTable. So the bar read "4 of 4" while the table showed 2, on every page, the moment any rule was active. Moving filtering into the columns is what introduced it. useTableChrome now exposes `shownRows`/`shownCount`, computed with the same engine the table uses, so the two cannot disagree. Fifteen call sites moved onto it, plus the DataTableFooter counts that told the same lie. ResourceListPage had a worse version: it sliced `items` for the page size and DataTable filtered afterwards, so a page size of 25 took 25 UNFILTERED rows and then filtered them -- "show 25" could render three. It pages `chrome.shownRows` now, so the slice and the render agree. The Security tabs already computed this correctly by calling applyFilters themselves; they are left alone rather than churned. Guarded going forward: the preset prober now reads `.sk-viewbar__count` after selecting each view and fails on !BARCOUNT if the number disagrees with the rendered rows. 0 mismatches across 11 targets. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/components/ds/grid/useTableChrome.js | 14 +++++++++++++- .../components/layouts/ResourceListPage.jsx | 18 +++++++++++------- .../src/components/settings/ApiSettingsTab.jsx | 2 +- .../src/components/settings/InvitationsTab.jsx | 2 +- .../settings/MigrationHistoryTab.jsx | 2 +- frontend/src/components/settings/UsersTab.jsx | 2 +- frontend/src/pages/CronJobs.jsx | 4 ++-- frontend/src/pages/DeliveryLog.jsx | 2 +- frontend/src/pages/FleetProxy.jsx | 2 +- frontend/src/pages/Incidents.jsx | 2 +- frontend/src/pages/Jobs.jsx | 4 ++-- frontend/src/pages/Monitors.jsx | 2 +- frontend/src/pages/QueueDetail.jsx | 2 +- frontend/src/pages/QueueOperations.jsx | 2 +- frontend/src/pages/Servers.jsx | 4 ++-- frontend/src/pages/Telemetry.jsx | 2 +- 16 files changed, 41 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/ds/grid/useTableChrome.js b/frontend/src/components/ds/grid/useTableChrome.js index 17d15358..ec98acc6 100644 --- a/frontend/src/components/ds/grid/useTableChrome.js +++ b/frontend/src/components/ds/grid/useTableChrome.js @@ -1,6 +1,6 @@ import { useCallback, useMemo, useState } from 'react'; import useGridViews from './useGridViews'; -import { OPS, emptyValueFor, isFilterable, ruleId, withInferredTypes } from './fields'; +import { OPS, applyFilters, emptyValueFor, isFilterable, ruleId, withInferredTypes } from './fields'; const NO_FILTERS = { match: 'all', rules: [] }; @@ -149,12 +149,24 @@ export function useTableChrome({ resetToView: api.resetToView, }); + // What the table will ACTUALLY render. `rows` is the page's own already + // searched/filtered array; the column rules are applied by + // itself, so a page counting `rows.length` for its view bar reported "4 of + // 4" while the table showed 2. Computed here, once, from the same engine + // the table uses, so the two can never disagree. + const shownRows = useMemo( + () => applyFilters(rows, filters, orderedColumns), + [rows, filters, orderedColumns], + ); + return { cfg, api, views, columns: orderedColumns, noun, + shownRows, + shownCount: shownRows.length, filterCount: filters.rules.length, drawerOpen, setDrawerOpen, diff --git a/frontend/src/components/layouts/ResourceListPage.jsx b/frontend/src/components/layouts/ResourceListPage.jsx index b452dbd7..d579bc61 100644 --- a/frontend/src/components/layouts/ResourceListPage.jsx +++ b/frontend/src/components/layouts/ResourceListPage.jsx @@ -173,11 +173,6 @@ export default function ResourceListPage({ } }; - const pagedItems = useMemo( - () => (pageSize === 'all' ? items : items.slice(0, pageSize)), - [items, pageSize], - ); - // The chrome — view picker, chip bar, filter drawer, tools menu, shareable // links — comes from the shared hook rather than being adapted again here. // This wrapper used to carry its own copy of that adapter, which is how it @@ -218,6 +213,15 @@ export default function ResourceListPage({ const orderedColumns = chrome.columns; const tableViews = chrome.views; + // Paged AFTER the column rules, not before. Slicing `items` meant a page + // size of 25 took 25 unfiltered rows and THEN filtered them, so "show 25" + // could render three. `chrome.shownRows` is the same set the table renders, + // so the two agree; DataTable re-applying the rules to it is a no-op. + const pagedItems = useMemo( + () => (pageSize === 'all' ? chrome.shownRows : chrome.shownRows.slice(0, pageSize)), + [chrome.shownRows, pageSize], + ); + if (loading) { // Same wrapper as the loaded state below. A skeleton that renders // outside the page's padded container occupies a different box than the @@ -260,7 +264,7 @@ export default function ResourceListPage({ )} @@ -365,7 +369,7 @@ export default function ResourceListPage({ footer={( { { { { { { rowClassName={(job) => (job.enabled ? undefined : 'is-disabled')} footer={( diff --git a/frontend/src/pages/DeliveryLog.jsx b/frontend/src/pages/DeliveryLog.jsx index 91d11a54..e689b9a6 100644 --- a/frontend/src/pages/DeliveryLog.jsx +++ b/frontend/src/pages/DeliveryLog.jsx @@ -268,7 +268,7 @@ export default function DeliveryLog() { { diff --git a/frontend/src/pages/Jobs.jsx b/frontend/src/pages/Jobs.jsx index bca51ac5..0380e28f 100644 --- a/frontend/src/pages/Jobs.jsx +++ b/frontend/src/pages/Jobs.jsx @@ -489,8 +489,8 @@ export default function Jobs() { views={chrome.views} label="jobs" total={hasFilters - ? `${jobs.length} of ${total} matching · ${allJobs} jobs total` - : `${jobs.length} of ${allJobs} jobs`} + ? `${chrome.shownCount} of ${total} matching · ${allJobs} jobs total` + : `${chrome.shownCount} of ${allJobs} jobs`} onCreate={chrome.createView} /> - {monitors.length} monitor{monitors.length === 1 ? '' : 's'} + {chrome.shownCount} of {monitors.length} monitor{monitors.length === 1 ? '' : 's'} {stats?.overall_uptime_30d != null && ( <> · {stats.overall_uptime_30d}% uptime (30d) )} diff --git a/frontend/src/pages/QueueDetail.jsx b/frontend/src/pages/QueueDetail.jsx index 94713b8d..4602a827 100644 --- a/frontend/src/pages/QueueDetail.jsx +++ b/frontend/src/pages/QueueDetail.jsx @@ -389,7 +389,7 @@ const QueueDetail = () => { diff --git a/frontend/src/pages/QueueOperations.jsx b/frontend/src/pages/QueueOperations.jsx index 030dc04c..71843b09 100644 --- a/frontend/src/pages/QueueOperations.jsx +++ b/frontend/src/pages/QueueOperations.jsx @@ -565,7 +565,7 @@ const QueueOperations = () => { diff --git a/frontend/src/pages/Servers.jsx b/frontend/src/pages/Servers.jsx index 93e973e9..4951b247 100644 --- a/frontend/src/pages/Servers.jsx +++ b/frontend/src/pages/Servers.jsx @@ -371,7 +371,7 @@ const Servers = () => { { tableClassName="servers-table" footer={( diff --git a/frontend/src/pages/Telemetry.jsx b/frontend/src/pages/Telemetry.jsx index 9544f168..ceb0f833 100644 --- a/frontend/src/pages/Telemetry.jsx +++ b/frontend/src/pages/Telemetry.jsx @@ -377,7 +377,7 @@ export default function Telemetry() {