diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index fd732cb89..3cb0a34b9 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,20 +1,112 @@ { "version": 1, - "base_sha": "ba4d48b4e604f31da815de4403afb62322d2694d", - "verified_at": "2026-08-20T19:59:00Z", + "base_sha": "df1290066e2907b344b9294d0106dcbabde03a53", + "verified_at": "2026-08-20T20:51:48Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Workloads/useWorkloadsState.ts", - "frontend-modern/src/components/Workloads/workloadSelectors.ts" + "frontend-modern/src/features/docker/DockerAlertsTable.tsx", + "frontend-modern/src/features/docker/DockerConfigsTable.tsx", + "frontend-modern/src/features/docker/DockerContainersTable.tsx", + "frontend-modern/src/features/docker/DockerHostsTable.tsx", + "frontend-modern/src/features/docker/DockerImagesTable.tsx", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx", + "frontend-modern/src/features/docker/DockerPageSurface.tsx", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx", + "frontend-modern/src/features/docker/DockerServicesTable.tsx", + "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx", + "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx", + "frontend-modern/src/features/docker/DockerTasksTable.tsx", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx", + "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx", + "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", + "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx", + "frontend-modern/src/features/vmware/VsphereActivityTable.tsx", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx", + "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx", + "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx" ], "content_sha256": { - "frontend-modern/src/components/Workloads/useWorkloadsState.ts": "333dcef3a4316afab0fb62514f4741469aaff1bb22477ba80c22c4a7313d982d", - "frontend-modern/src/components/Workloads/workloadSelectors.ts": "8b0a8724bfe79bba0b573eb1da5c725adeda1e3e6443f011ba41fd3b786e8a49" + "frontend-modern/src/features/docker/DockerAlertsTable.tsx": "0b9bea8a536a60c1fb9a6dc0ba11ff37400dcf39e35197893b37b7418dcbaa28", + "frontend-modern/src/features/docker/DockerConfigsTable.tsx": "d0953a8f2a5ca3bfd588d4d76945cfe880f484a76dbb49a03e66d33f8e943c5d", + "frontend-modern/src/features/docker/DockerContainersTable.tsx": "725833c041e9cac2a0df0407ca726352d25e80a9483ae1ffc171db8b899cded6", + "frontend-modern/src/features/docker/DockerHostsTable.tsx": "f2080e90ec27178ef11d58164b5dc0055d968150d06998b0770a23b9bcf36826", + "frontend-modern/src/features/docker/DockerImagesTable.tsx": "81e2559e29e71f193af0410446af6ad6fe46e2f01ecc0799ea534f591ea9352b", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "ec4ff5168ffa2405a8b818bb85cfbcd639adfc530965db7aaf80532306cfa121", + "frontend-modern/src/features/docker/DockerPageSurface.tsx": "e789ff025a3c8ecf74f57d43122d32f9128ea1da4fabd161f1f2e64fe42b763e", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx": "1453356c100f6d38102fcc6ade351d97e7fd06efb580cd34a4b1d2ac3b218aae", + "frontend-modern/src/features/docker/DockerServicesTable.tsx": "250ded4ce0cbeda16427b5f47e3e289ff09b5d43dcc3c9d9ae80a356fbfac2b6", + "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx": "b37f1b96a094979ac734a0f9647ca2558d791a9646bf0ccf7b8b8682202454c7", + "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx": "47bade17b070470a77e8cd8adc9f56072d91e0ef975a2311a163fc8d4291db3d", + "frontend-modern/src/features/docker/DockerTasksTable.tsx": "4c2832daa4f82f9a4c6961dda3a90d0af416286f06f2ad98145f734e5b21a253", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx": "a1111ff720bbb2b110617e2e64d2ea0229cdd731db18228769e857a9c3c6b641", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx": "6f5f291b9b1b038e4f5268ef39e52683c212b814dae3fc4ef373c06957b872b0", + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "bd74274b3778753be5994bb14b1245110835687978ad9c4d88c282723d941ab3", + "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx": "34633caacfb7c58679c66cd609301237f585de41b38e1219aa358c77583b668f", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "7be2ad7c56fd26086c998b8bcfe65641d4fa34ccc243fe725619728d7438add6", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "24fc9b3ccab27da44e3c4b6ef10d3ad1c09672883f152195ef6c63a96e15f31e", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "220ab81ca584476c83148e6b058f034bd52052749a33469933b11b1ef9a07b5e", + "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "9ae69de55dbbe5a8ebc1a93418357a09791db61119af67027bf7641de040929e", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "0887ec3a7a364b6b55f8ef76ed764e9c1ae8d286fc3cc64fabb5e1c4fc31ed18", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "d1b3ad43e54226d158268076b80e2935767778eee10a29e57957ea8ea1c44281", + "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx": "d1431b557cb8fa6ad1b2c90318add989456caddd8b9a255b65267dd20e517746", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "b03bfe0320d1c515b2d70fa5f2769738f5fdc1cb085834d442e18bb198357273", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "2fb1e679cf4b611d208eb03659f21c2d75835e4e93cdc94376d28d8989434775", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "77c419e6bd321768a8b1112e512dc91902cd9a39eb248de401555e8b447d138a", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "f24b0732fdf65f8d1308ea1e3ecaa752de6c103a93e99c529e8ba3cc1b8b75a1", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "d8f9117b9d0c8ff5b1b50d3bdd26cfb9ad83f67522d6df646c6051c1aeed1106", + "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "df294240ac6089d57314743658912ba0667a763ac09d977f79188be3e948bfae", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "763a6a2a2dfc8b66469e0536a9f7a042f675ba507ad4aee784a41b0897712075", + "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "67670bc9d65cdd2eb602e016aec7ccc70cf52b5783aafbabb0fa3e268eda3c5c", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "4f0772c77e399f8e7f191448b6eb4802a2c88092323d5423a68b91c5be425c7a", + "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx": "564c594d35e71fead4c408e3342b0ed29205cebfe2e087b4dbc54079e73c159f", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx": "7560b41d9539fa92e4d678cd007b994065a1a16e9a08084b587900d052432736", + "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx": "d450be7beb823f1222cc9d5ac86a54802141d9623ffa5145a7feee9482b6d05d", + "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx": "328caf9474494ac639888c4800abfe3ce5e900ec9203edc7211fbf126153ae6a", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx": "bcc75ab5bab12b21f00212b18679ddcce9f5a603f212991581ccdc07109ef15c", + "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx": "85f9677ef2a2ca3ad739c3dcfa2f127b9274ba43ac12452d67b7a44bbcaff6d6", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "105e87bc673d3bd8f5e1b975d14f8b99f7a60233002b4fd89678c0813bf432a8", + "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "458e2af554abd3c60e0e2af51eb2e4d95a9b74615245dd54d76d611ef0704371", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "e521207945aa2ba5ae274d7618c90febe8365d05c854435830f29083cf482b83", + "frontend-modern/src/features/vmware/VsphereActivityTable.tsx": "5e6093781558567bbf925f6862220b6b40d4420d438fd1f797282d7b3a0625f8", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx": "ef4fd7067ca699b4e3f1060874180daa0bb7554d9c439626a8a9553d14d67c64", + "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx": "2a4150e146169c192b25591934ed2a84f2b57e7ea47af41fe169f8255751f1e4", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "c5bd0a61c793835490c2555d184d8d54b8d30f8fa6cc6d03ccf7c39fdafb5263", + "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "61eb9935878eee11f15834f98832149ad40e8b868650a7420e0d03135436e922" }, "routes": [ - "/proxmox/overview", - "/vsphere (via nav tab)", - "/kubernetes (via nav tab)" + "/docker/overview", + "/kubernetes/workloads", + "/truenas (via nav tab)", + "/standalone/machines (via nav tab)", + "/vmware/overview (via nav tab)", + "/proxmox/replication", + "/proxmox/ceph" ], "viewports": [ { @@ -27,17 +119,21 @@ } ], "states": [ - "Proxmox Overview chips scoped to platform inventory: All 25 / VMs 10 / LXCs 15 and All 25 / Running 23 / Attention 0 / Stopped 2, matching the nodes table (10 VMs + 15 CTs); pre-fix estate-wide counts (All 55, Attention 1) no longer appear", - "Attention filter active on Proxmox Overview: chip count 0 and 'No guests found' empty state are now consistent (count and table read the same platform-scoped set)", - "Add filter node dropdown on Proxmox Overview lists only pve1/pve2/pve3 (no off-platform hosts)", - "vSphere Overview chips scoped: All 18 / Powered on 18 / Attention 0 / Powered off 0", - "Kubernetes Overview still surfaces the Pending cron-nightly-backfill pod as '1 workload needs attention' (the workload whose degraded status previously leaked into the Proxmox Attention chip)", - "Mobile viewport (375x812) Proxmox Overview renders nodes table, Filters button with active-count badge, and honest empty state while the persisted Attention filter was active" + "Docker Overview status chips now carry live counts (All 12 / Healthy 12 / Degraded 0 / Offline 0) matching the '12 containers' toolbar counter", + "Docker Overview with Degraded selected: chip count 0 agrees with the '0 of 12 containers' counter and the filtered empty state", + "Kubernetes Workloads shared toolbar counts across all four stacked sections (All 19 / Healthy 17 / Degraded 2 / Offline 0); selecting Degraded shows '2 of 19 rows' including the Pending cron-nightly-backfill pod, so the chip count equals the rows its selection renders", + "TrueNAS incident severity chips counted (All 2 / Critical 0 / Warning 2 / Info 0)", + "Machines (standalone) chips counted (All 2 / Healthy 2 / Degraded 0 / Offline 0)", + "vSphere overview workloads chips unchanged and still counted (All 18 / Powered on 18 / Attention 0 / Powered off 0)", + "Proxmox Replication bespoke toolbar counted (All 8 / Healthy 8 / Failed 0 / Pending 0 / Disabled 0) and Proxmox Ceph counted (All 1 / Healthy 0 / Warning 1 / Critical 0)", + "Inventory totals preference set to hidden: chips reactively drop their counts (plain All / Healthy / Degraded / Offline) along with the rows counter; restoring the preference brings counts back on remount", + "Mobile viewport (375x812) Docker Overview renders with chips collapsed behind the canonical Filters control" ], "interactions": [ - "Clicked the Attention status chip on Proxmox Overview and observed chip count 0 with the consistent 'No guests found' empty state", - "Opened the Add filter dropdown options on Proxmox Overview and read the node list", - "Navigated Proxmox -> vSphere -> Kubernetes nav tabs and read each overview's chip counts", - "Reset the persisted status filter back to All and confirmed the guest table renders 25 workloads grouped by node" + "Clicked the Degraded chip on Docker Overview and confirmed the empty state plus '0 of 12 containers'", + "Clicked the Degraded chip on Kubernetes Workloads and confirmed '2 of 19 rows' with the Pending pod visible", + "Navigated Docker -> Kubernetes -> TrueNAS -> Machines -> vSphere -> Proxmox Replication -> Proxmox Ceph reading each toolbar's chip names and counts from the accessibility tree", + "Toggled the shared platformEstateOverviewVisible preference off and back on, confirming the chip-count gate follows it", + "Reset the Docker status filter back to All before finishing" ] } diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 168ce4588..3d6fd8741 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -5290,11 +5290,13 @@ describe('shared primitive guardrails', () => { }); it('keeps table-local attention counts on the shared platform filter template', () => { - expect(sharedPlatformPageSource).toContain('export function withPlatformAttentionCount'); - expect(sharedPlatformPageSource).toContain(' = (props) => searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="configs" diff --git a/frontend-modern/src/features/docker/DockerContainersTable.tsx b/frontend-modern/src/features/docker/DockerContainersTable.tsx index 13b691be0..d4af30d98 100644 --- a/frontend-modern/src/features/docker/DockerContainersTable.tsx +++ b/frontend-modern/src/features/docker/DockerContainersTable.tsx @@ -43,6 +43,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -346,11 +347,15 @@ export const DockerContainersTable: Component = (pro }, ]; }); - const scopedRows = createMemo(() => { + const applyHostScope = (rows: Resource[]) => { const host = hostFilter(); - const base = tableState.filtered(); - return host ? base.filter((resource) => dockerHostName(resource) === host) : base; - }); + return host ? rows.filter((resource) => dockerHostName(resource) === host) : rows; + }; + const scopedRows = createMemo(() => applyHostScope(tableState.filtered())); + // The host facet narrows rows after the shared filter state, so chip counts + // must apply it too or a scoped view would count other hosts' containers. + const countForStatus = (value: DockerResourceStatusFilter): number => + applyHostScope(filterDockerResources(props.resources, tableState.search(), value)).length; const hasActiveFilters = () => tableState.hasActiveFilters() || hostFilter() !== ''; // Reset must clear every URL param in ONE navigation. Consecutive // setSearchParams calls each merge against the pre-navigation URL (the @@ -730,7 +735,7 @@ export const DockerContainersTable: Component = (pro searchTips={DOCKER_CONTAINER_SEARCH_TIPS} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(PLATFORM_HEALTH_FILTER_OPTIONS, countForStatus)} filters={scopeFilters()} visible={scopedRows().length} total={tableState.total()} diff --git a/frontend-modern/src/features/docker/DockerHostsTable.tsx b/frontend-modern/src/features/docker/DockerHostsTable.tsx index afefbe2a8..e2065cc85 100644 --- a/frontend-modern/src/features/docker/DockerHostsTable.tsx +++ b/frontend-modern/src/features/docker/DockerHostsTable.tsx @@ -30,6 +30,7 @@ import { getPlatformTableFiniteMetric, getPlatformTableCellClassForKind, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow'; import type { Disk } from '@/types/api'; @@ -213,7 +214,10 @@ export const DockerHostsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="hosts" diff --git a/frontend-modern/src/features/docker/DockerImagesTable.tsx b/frontend-modern/src/features/docker/DockerImagesTable.tsx index 6ee85c5f9..78a9c335a 100644 --- a/frontend-modern/src/features/docker/DockerImagesTable.tsx +++ b/frontend-modern/src/features/docker/DockerImagesTable.tsx @@ -11,6 +11,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -113,7 +114,10 @@ export const DockerImagesTable: Component< searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="images" diff --git a/frontend-modern/src/features/docker/DockerNetworksTable.tsx b/frontend-modern/src/features/docker/DockerNetworksTable.tsx index 08ba2950c..4af12600c 100644 --- a/frontend-modern/src/features/docker/DockerNetworksTable.tsx +++ b/frontend-modern/src/features/docker/DockerNetworksTable.tsx @@ -14,6 +14,7 @@ import { getPlatformTableHeadClassForKind, type PlatformTableContainerLayout, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { useObservedElementWidth } from '@/hooks/useObservedElementWidth'; import { @@ -109,24 +110,23 @@ const ATTACHMENT_GROUPS: readonly AttachmentGroup[] = [ { key: 'running', label: 'Running', description: 'No active issue reported' }, ] as const; +// No static ariaLabel here: the shared chip naming (label plus live count) +// is the accessible name, and the titles carry the descriptions. const ATTACHMENT_STATUS_FILTER_OPTIONS: PlatformTableFilterOption[] = [ - { value: 'all', label: 'All', ariaLabel: 'All', title: 'All attached containers' }, + { value: 'all', label: 'All', title: 'All attached containers' }, { value: 'attention', label: 'Attention', - ariaLabel: 'Attention', title: 'Containers that need review', }, { value: 'running', label: 'Running', - ariaLabel: 'Running', title: 'Running containers', }, { value: 'other', label: 'Other', - ariaLabel: 'Other', title: 'Stopped, paused, or unknown containers', }, ]; @@ -301,7 +301,10 @@ const AttachmentDetail: Component<{ rows: readonly DockerNetworkAttachmentRow[] searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={setStatus} - statusOptions={ATTACHMENT_STATUS_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + ATTACHMENT_STATUS_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="containers" @@ -445,7 +448,10 @@ export const DockerNetworksTable: Component = (props) searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="networks" diff --git a/frontend-modern/src/features/docker/DockerPageSurface.tsx b/frontend-modern/src/features/docker/DockerPageSurface.tsx index b88434b62..d363b73b9 100644 --- a/frontend-modern/src/features/docker/DockerPageSurface.tsx +++ b/frontend-modern/src/features/docker/DockerPageSurface.tsx @@ -11,6 +11,7 @@ import { PlatformTableEmptyState, PlatformTableLoadingState, PlatformTableToolbar, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { DockerAlertsTable } from './DockerAlertsTable'; import { DockerConfigsTable } from './DockerConfigsTable'; @@ -210,11 +211,12 @@ function DockerStorage(props: { model: DockerPageModel }) { buildPlatformResourceSearchSuggestions([...storageHosts(), ...props.model.volumes]), ); const totalRows = createMemo(() => storageHosts().length + props.model.volumes.length); - const visibleRows = createMemo( - () => - filterDockerResources(storageHosts(), search(), status()).length + - filterDockerResources(props.model.volumes, search(), status()).length, - ); + // The toolbar governs both stacked tables, so visible rows and chip counts + // sum the same per-section predicate. + const countForStatus = (value: DockerResourceStatusFilter): number => + filterDockerResources(storageHosts(), search(), value).length + + filterDockerResources(props.model.volumes, search(), value).length; + const visibleRows = createMemo(() => countForStatus(status())); const hasActiveFilters = createMemo(() => search().trim().length > 0 || status() !== 'all'); const resetFilters = () => { setSearch(''); @@ -240,7 +242,7 @@ function DockerStorage(props: { model: DockerPageModel }) { searchSuggestions={searchSuggestions} status={status()} onStatusChange={setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(PLATFORM_HEALTH_FILTER_OPTIONS, countForStatus)} visible={visibleRows()} total={totalRows()} rowNoun="rows" diff --git a/frontend-modern/src/features/docker/DockerSecretsTable.tsx b/frontend-modern/src/features/docker/DockerSecretsTable.tsx index f3626dc7c..dca171244 100644 --- a/frontend-modern/src/features/docker/DockerSecretsTable.tsx +++ b/frontend-modern/src/features/docker/DockerSecretsTable.tsx @@ -13,6 +13,7 @@ import { getPlatformTableDateTimeSortValue, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -94,7 +95,10 @@ export const DockerSecretsTable: Component = (props) => searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="secrets" diff --git a/frontend-modern/src/features/docker/DockerServicesTable.tsx b/frontend-modern/src/features/docker/DockerServicesTable.tsx index 902cd835b..3d0427281 100644 --- a/frontend-modern/src/features/docker/DockerServicesTable.tsx +++ b/frontend-modern/src/features/docker/DockerServicesTable.tsx @@ -14,6 +14,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -173,7 +174,10 @@ export const DockerServicesTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="services" diff --git a/frontend-modern/src/features/docker/DockerStorageUsageTable.tsx b/frontend-modern/src/features/docker/DockerStorageUsageTable.tsx index 35ce2d24d..2a42ed40b 100644 --- a/frontend-modern/src/features/docker/DockerStorageUsageTable.tsx +++ b/frontend-modern/src/features/docker/DockerStorageUsageTable.tsx @@ -19,6 +19,7 @@ import { getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import type { DockerStorageUsageMeta, Resource } from '@/types/resource'; import { @@ -94,7 +95,10 @@ export const DockerStorageUsageTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="hosts" diff --git a/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx b/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx index 162288c77..417fc020d 100644 --- a/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx +++ b/frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx @@ -13,6 +13,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -125,7 +126,10 @@ export const DockerSwarmNodesTable: Component = (props) searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="nodes" diff --git a/frontend-modern/src/features/docker/DockerTasksTable.tsx b/frontend-modern/src/features/docker/DockerTasksTable.tsx index 65232cf64..a13b6870d 100644 --- a/frontend-modern/src/features/docker/DockerTasksTable.tsx +++ b/frontend-modern/src/features/docker/DockerTasksTable.tsx @@ -13,6 +13,7 @@ import { getPlatformTableDateTimeSortValue, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -108,7 +109,10 @@ export const DockerTasksTable: Component = (props) => { searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="tasks" diff --git a/frontend-modern/src/features/docker/DockerVolumesTable.tsx b/frontend-modern/src/features/docker/DockerVolumesTable.tsx index 5d4110625..2ef4a2ce1 100644 --- a/frontend-modern/src/features/docker/DockerVolumesTable.tsx +++ b/frontend-modern/src/features/docker/DockerVolumesTable.tsx @@ -14,6 +14,7 @@ import { getPlatformTableDateTimeSortValue, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -111,7 +112,10 @@ export const DockerVolumesTable: Component = (props) => searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="volumes" diff --git a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx index e36ac1a23..9a0bd5abb 100644 --- a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx +++ b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx @@ -1096,7 +1096,7 @@ describe('Docker native tables', () => { expect(document.querySelectorAll('[data-docker-volume-row]')).toHaveLength(2); - fireEvent.click(screen.getByRole('button', { name: 'Offline' })); + fireEvent.click(screen.getByRole('button', { name: /^Offline, \d+$/ })); expect(screen.getByText('1 of 2 volumes')).toBeInTheDocument(); expect(document.querySelector('[data-docker-volume-row="volume-cache"]')).not.toBeNull(); @@ -1331,13 +1331,13 @@ describe('Docker native tables', () => { expect(detail.getByText('worker-29')).toBeInTheDocument(); expect(detail.getByText('Show first 24')).toBeInTheDocument(); - fireEvent.click(detail.getByRole('button', { name: 'Attention' })); + fireEvent.click(detail.getByRole('button', { name: /^Attention, \d+$/ })); expect(detail.getByText('2 containers of 30 containers')).toBeInTheDocument(); expect(detail.getByText('api-unhealthy')).toBeInTheDocument(); expect(detail.getByText('api-restarting')).toBeInTheDocument(); expect(detail.queryByText('worker-29')).toBeNull(); - fireEvent.click(detail.getByRole('button', { name: 'All' })); + fireEvent.click(detail.getByRole('button', { name: /^All, \d+$/ })); fireEvent.input(detail.getByPlaceholderText('Search attached containers'), { target: { value: 'worker-29' }, }); diff --git a/frontend-modern/src/features/docker/__tests__/DockerPageSurface.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerPageSurface.test.tsx index de3a4b488..4fa40ef57 100644 --- a/frontend-modern/src/features/docker/__tests__/DockerPageSurface.test.tsx +++ b/frontend-modern/src/features/docker/__tests__/DockerPageSurface.test.tsx @@ -639,7 +639,7 @@ describe('DockerPageSurface', () => { 'checkout', ); - fireEvent.click(screen.getByRole('button', { name: 'Offline' })); + fireEvent.click(screen.getByRole('button', { name: /^Offline, \d+$/ })); expect(screen.getByText('1 of 2 rows')).toBeInTheDocument(); expect(screen.getByTestId('docker-storage-usage-table')).toHaveAttribute( diff --git a/frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx index c9839719d..abd4152a1 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx @@ -16,6 +16,7 @@ import { getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -142,7 +143,10 @@ export const KubernetesAlertsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={KUBERNETES_INCIDENT_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + KUBERNETES_INCIDENT_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="alerts" diff --git a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx index 70da63eeb..5c9611c82 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx @@ -17,6 +17,7 @@ import { summarizePlatformTableValues, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -141,7 +142,10 @@ export const KubernetesAutoscalingTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="autoscalers" diff --git a/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx index aced03fc4..06a20502a 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx @@ -24,6 +24,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -154,7 +155,10 @@ export const KubernetesClustersTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="clusters" diff --git a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx index 94c59cb5a..eecf1f8da 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx @@ -16,6 +16,7 @@ import { summarizePlatformTableValues, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -233,7 +234,10 @@ export const KubernetesConfigTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="config resources" diff --git a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx index 4909a5019..d3ed73629 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx @@ -16,6 +16,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -252,7 +253,10 @@ export const KubernetesControllersTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="controllers" diff --git a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx index 6264c2261..4467e35f5 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx @@ -18,6 +18,7 @@ import { getPlatformTableDateTimeSortValue, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -141,7 +142,10 @@ export const KubernetesDeploymentsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="deployments" diff --git a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx index 68dfca936..b1195756c 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx @@ -14,6 +14,7 @@ import { getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -85,7 +86,10 @@ export const KubernetesEventsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="events" diff --git a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx index 1706c2047..7cae9da19 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx @@ -17,6 +17,7 @@ import { summarizePlatformTableValues, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -177,7 +178,10 @@ export const KubernetesNetworkingTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="network resources" diff --git a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx index f1f4cfb47..af9eb1730 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx @@ -26,6 +26,7 @@ import { getPlatformTableFiniteMetric, getPlatformTableCellClassForKind, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -168,7 +169,10 @@ export const KubernetesNodesTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="nodes" diff --git a/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx b/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx index 9a2b98a94..859ec609c 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx @@ -19,6 +19,7 @@ import { PlatformTableEmptyState, PlatformTableLoadingState, PlatformTableToolbar, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import type { Resource } from '@/types/resource'; import { buildPlatformResourceSearchSuggestions } from '@/features/platformPage/platformSearchSuggestions'; @@ -380,9 +381,9 @@ function KubernetesWorkloads(props: { const scopedControllers = () => scope.scopedSections()[2]; const scopedAutoscaling = () => scope.scopedSections()[3]; const scopeFilters = scope.scopeFilters; - const visibleRows = createMemo(() => - countKubernetesVisible(scope.scopedSections(), toolbar.search(), toolbar.status()), - ); + const countForStatus = (value: KubernetesResourceStatusFilter): number => + countKubernetesVisible(scope.scopedSections(), toolbar.search(), value); + const visibleRows = createMemo(() => countForStatus(toolbar.status())); const hasActiveFilters = () => toolbar.hasActiveFilters() || scope.hasActiveScope(); const resetFilters = () => toolbar.resetFilters({ @@ -409,7 +410,7 @@ function KubernetesWorkloads(props: { searchSuggestions={searchSuggestions} status={toolbar.status()} onStatusChange={toolbar.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(PLATFORM_HEALTH_FILTER_OPTIONS, countForStatus)} filters={scopeFilters()} leadingControls={ 0}> @@ -499,9 +500,9 @@ function KubernetesServices(props: { model: KubernetesPageModel }) { const scope = createKubernetesInventoryScope(sections); const scopedServices = () => scope.scopedSections()[0]; const scopedNetworking = () => scope.scopedSections()[1]; - const visibleRows = createMemo(() => - countKubernetesVisible(scope.scopedSections(), toolbar.search(), toolbar.status()), - ); + const countForStatus = (value: KubernetesResourceStatusFilter): number => + countKubernetesVisible(scope.scopedSections(), toolbar.search(), value); + const visibleRows = createMemo(() => countForStatus(toolbar.status())); const hasActiveFilters = () => toolbar.hasActiveFilters() || scope.hasActiveScope(); const resetFilters = () => toolbar.resetFilters({ @@ -528,7 +529,7 @@ function KubernetesServices(props: { model: KubernetesPageModel }) { searchSuggestions={searchSuggestions} status={toolbar.status()} onStatusChange={toolbar.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(PLATFORM_HEALTH_FILTER_OPTIONS, countForStatus)} filters={scope.scopeFilters()} visible={visibleRows()} total={totalRows()} @@ -576,9 +577,9 @@ function KubernetesConfiguration(props: { model: KubernetesPageModel }) { const scope = createKubernetesInventoryScope(sections); const scopedConfig = () => scope.scopedSections()[0]; const scopedPolicy = () => scope.scopedSections()[1]; - const visibleRows = createMemo(() => - countKubernetesVisible(scope.scopedSections(), toolbar.search(), toolbar.status()), - ); + const countForStatus = (value: KubernetesResourceStatusFilter): number => + countKubernetesVisible(scope.scopedSections(), toolbar.search(), value); + const visibleRows = createMemo(() => countForStatus(toolbar.status())); const hasActiveFilters = () => toolbar.hasActiveFilters() || scope.hasActiveScope(); const resetFilters = () => toolbar.resetFilters({ @@ -605,7 +606,7 @@ function KubernetesConfiguration(props: { model: KubernetesPageModel }) { searchSuggestions={searchSuggestions} status={toolbar.status()} onStatusChange={toolbar.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(PLATFORM_HEALTH_FILTER_OPTIONS, countForStatus)} filters={scope.scopeFilters()} visible={visibleRows()} total={totalRows()} diff --git a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx index daa6a38c6..c624184e0 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx @@ -15,6 +15,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -184,7 +185,10 @@ export const KubernetesPodsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="pods" diff --git a/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx index 1d6f4f198..1396b2a17 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx @@ -14,6 +14,7 @@ import { getPlatformTableHeadClassForKind, summarizePlatformTableValues, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -215,7 +216,10 @@ export const KubernetesPolicyTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="policy resources" diff --git a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx index e614fabbd..7d699d592 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx @@ -17,6 +17,7 @@ import { summarizePlatformTableValues, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -135,7 +136,10 @@ export const KubernetesServicesTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="services" diff --git a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx index a47c00049..8d4ae3927 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx @@ -16,6 +16,7 @@ import { getPlatformTableCellClassForKind, PlatformTableShell, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -247,7 +248,10 @@ export const KubernetesStorageTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="storage resources" diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index 2a8b32d00..b890aaa25 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -174,7 +174,7 @@ const proxmoxInlineDetailTableSources = [ describe('platform overview layout guardrails', () => { it('keeps attention summaries canonical without a competing estate panel', () => { for (const source of [truenasProtectionTableSource, vsphereAlertsTableSource]) { - expect(source).toContain('withPlatformAttentionCount'); + expect(source).toContain('withPlatformStatusCounts'); expect(source).not.toContain('PlatformAttentionSummary'); } for (const source of overviewSurfaceSources) { diff --git a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts index 231409940..398c2a7fc 100644 --- a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts @@ -34,7 +34,7 @@ import { getPlatformTableWeightedColumnWidthStyle, normalizePlatformResourceStatusFilter, summarizePlatformTableValues, - withPlatformAttentionCount, + withPlatformStatusCounts, type PlatformResourceStatusFilter, } from '../sharedPlatformPage'; @@ -43,39 +43,41 @@ afterEach(() => { window.localStorage.clear(); }); -describe('withPlatformAttentionCount', () => { - it('decorates one attention option with a compact accessible count', () => { - let attention: ReturnType>[number] | undefined; - render(() => { - const options = withPlatformAttentionCount( - [ - { value: 'all', label: 'All' }, - { value: 'attention', label: 'Attention', tone: 'warning' }, - ], - { value: 'attention', count: 2, tone: 'danger', noun: 'health signal' }, - ); - attention = options[1]; - return attention.visualLabel; - }); - - expect(attention).toMatchObject({ - ariaLabel: 'Attention, 2 health signals', - title: 'Show 2 health signals', - tone: 'danger', - leading: undefined, - }); - - expect(document.body).toHaveTextContent('Attention2'); - }); - - it('leaves the base options undecorated when there is no attention', () => { - const options = withPlatformAttentionCount( - [{ value: 'attention', label: 'Attention', tone: 'warning' }], - { value: 'attention', count: 0, tone: 'warning', noun: 'issue' }, +describe('withPlatformStatusCounts', () => { + it('gives every option the count its own filter selection would render', () => { + const counts: Record = { all: 5, attention: 2, healthy: 3 }; + const options = withPlatformStatusCounts( + [ + { value: 'all', label: 'All' }, + { value: 'attention', label: 'Attention', tone: 'warning' }, + { value: 'healthy', label: 'Healthy', tone: 'success' }, + ], + (value) => counts[value], ); - expect(options[0].visualLabel).toBeUndefined(); - expect(options[0].ariaLabel).toBeUndefined(); + expect(options.map((option) => option.count)).toEqual([5, 2, 3]); + expect(options[1].tone).toBe('warning'); + }); + + it('escalates the attention tone only while that option has matches', () => { + const withMatches = withPlatformStatusCounts( + [{ value: 'attention', label: 'Attention', tone: 'warning' }], + () => 2, + { value: 'attention', tone: 'danger' }, + ); + expect(withMatches[0]).toMatchObject({ count: 2, tone: 'danger' }); + + const clear = withPlatformStatusCounts( + [{ value: 'attention', label: 'Attention', tone: 'warning' }], + () => 0, + { value: 'attention', tone: 'danger' }, + ); + expect(clear[0]).toMatchObject({ count: 0, tone: 'warning' }); + }); + + it('clamps malformed counts to zero', () => { + const options = withPlatformStatusCounts([{ value: 'all', label: 'All' }], () => Number.NaN); + expect(options[0].count).toBe(0); }); }); diff --git a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx index b2c5feebd..1e9503e86 100644 --- a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx +++ b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx @@ -21,7 +21,6 @@ import { EmptyState } from '@/components/shared/EmptyState'; import { type FilterOption as PlatformTableFilterOption } from '@/components/shared/FilterButtonGroup'; import { FilterBar, filterChipStatusDot, type FilterDef } from '@/components/shared/FilterBar'; import { FilterSegmentedControl } from '@/components/shared/FilterToolbar'; -import { MetadataBadge } from '@/components/shared/MetadataBadge'; import { type SearchInputProps, type SearchInputSuggestion } from '@/components/shared/SearchInput'; import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/shared/Table'; import { TableCard } from '@/components/shared/TableCard'; @@ -51,35 +50,25 @@ import { export type { PlatformTableFilterOption }; -export function withPlatformAttentionCount( +// Decorates every status option with the row count the table would show if +// that option were selected. Callers pass the same predicate the table's +// filter applies (createPlatformTableFilterState exposes it as +// countForStatus), so a chip's number can never disagree with the rows that +// clicking it renders. The optional attention entry escalates that option's +// tone while it has matches. +export function withPlatformStatusCounts( options: readonly PlatformTableFilterOption[], - config: { - value: T; - count: number; - tone: 'warning' | 'danger'; - noun: string; - }, + countForStatus: (value: T) => number, + attention?: { value: T; tone: 'warning' | 'danger' }, ): PlatformTableFilterOption[] { - const count = Number.isFinite(config.count) ? Math.max(0, Math.trunc(config.count)) : 0; - if (count === 0) return [...options]; - - const countedNoun = count === 1 ? config.noun : `${config.noun}s`; return options.map((option) => { - if (option.value !== config.value) return option; + const raw = countForStatus(option.value); + const count = Number.isFinite(raw) ? Math.max(0, Math.trunc(raw)) : 0; + const escalate = attention !== undefined && option.value === attention.value && count > 0; return { ...option, - ariaLabel: `${option.label}, ${count} ${countedNoun}`, - title: `Show ${count} ${countedNoun}`, - tone: config.tone, - leading: undefined, - visualLabel: ( - <> - {option.label} - - - ), + count, + ...(escalate ? { tone: attention.tone } : {}), }; }); } @@ -1154,6 +1143,11 @@ export function createPlatformTableFilterState props.filter(props.resources(), search(), status())); const visible = createMemo(() => filtered().length); const total = createMemo(() => props.resources().length); + // Chip-count source for withPlatformStatusCounts: runs the table's own + // predicate with the candidate status, so the count a chip shows is exactly + // the row set selecting it would render. + const countForStatus = (value: Status): number => + props.filter(props.resources(), search(), value).length; const hasActiveFilters = createMemo( () => search().trim().length > 0 || status() !== props.initialStatus, ); @@ -1177,6 +1171,7 @@ export function createPlatformTableFilterState(props: { visualLabel: option.visualLabel, icon: option.icon, tone: option.tone, + // The same estate-wide preference that gates the rows counter gates + // the chip counts, so "Inventory totals: Hide" quiets both. + count: inventoryCountsVisible() ? option.count : undefined, })), value: () => String(props.status), setValue: (value) => { @@ -1391,7 +1389,10 @@ export const PlatformResourceTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_STATUS_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_STATUS_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="rows" diff --git a/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx index 7e4cc606a..197ce6235 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx @@ -24,6 +24,7 @@ import { type PlatformTableFilterOption, PlatformTableEmptyState, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow'; import { useObservedElementWidth } from '@/hooks/useObservedElementWidth'; @@ -239,9 +240,8 @@ export const ProxmoxCephTable: Component<{ setSelectedId(null); }; - const filtered = createMemo(() => { + const filterByStatus = (want: CephStatusFilter) => { const split = splitSearchExclusions(search()); - const want = status(); return props.resources.filter((cluster) => { if (want !== 'all' && classify(cluster) !== want) return false; if (!split.needle && split.excludes.length === 0) return true; @@ -258,7 +258,9 @@ export const ProxmoxCephTable: Component<{ .toLowerCase(); return matchesSearchTermSplit(haystack, split); }); - }); + }; + const filtered = createMemo(() => filterByStatus(status())); + const countForStatus = (value: CephStatusFilter): number => filterByStatus(value).length; const total = createMemo(() => props.resources.length); const visible = createMemo(() => filtered().length); @@ -308,7 +310,7 @@ export const ProxmoxCephTable: Component<{ searchSuggestions={searchSuggestions} status={status()} onStatusChange={setStatus} - statusOptions={STATUS_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(STATUS_FILTER_OPTIONS, countForStatus)} visible={visible()} total={total()} rowNoun="clusters" diff --git a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx index ea4e57365..aedeef314 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx @@ -19,6 +19,7 @@ import { type PlatformResourceStatusFilter, PlatformTableEmptyState, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { useObservedElementWidth } from '@/hooks/useObservedElementWidth'; import { PlatformResourceDetailToggleButton } from '@/features/platformPage/PlatformResourceDetailTableRow'; @@ -124,7 +125,10 @@ export const ProxmoxMailGatewayTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="instances" diff --git a/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx index b3a43e937..93f8ecf6b 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx @@ -21,6 +21,7 @@ import { PlatformTableEmptyState, PlatformTableLoadingState, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { useObservedElementWidth } from '@/hooks/useObservedElementWidth'; import type { ReplicationJob, ReplicationJobsResponse } from '@/types/api'; @@ -275,9 +276,8 @@ export const ProxmoxReplicationTable: Component<{ const [status, setStatus] = createSignal('all'); const [expandedJobKey, setExpandedJobKey] = createSignal(null); - const filtered = createMemo(() => { + const filterByStatus = (want: ReplicationStatusFilter) => { const split = splitSearchExclusions(search()); - const want = status(); return (props.jobs ?? []).filter((job) => { if (want !== 'all' && classifyJob(job) !== want) return false; if (!split.needle && split.excludes.length === 0) return true; @@ -296,7 +296,9 @@ export const ProxmoxReplicationTable: Component<{ .toLowerCase(); return matchesSearchTermSplit(haystack, split); }); - }); + }; + const filtered = createMemo(() => filterByStatus(status())); + const countForStatus = (value: ReplicationStatusFilter): number => filterByStatus(value).length; const total = createMemo(() => (props.jobs ?? []).length); const visible = createMemo(() => filtered().length); @@ -385,7 +387,7 @@ export const ProxmoxReplicationTable: Component<{ searchSuggestions={searchSuggestions} status={status()} onStatusChange={setStatus} - statusOptions={STATUS_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts(STATUS_FILTER_OPTIONS, countForStatus)} visible={visible()} total={total()} rowNoun="jobs" diff --git a/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx b/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx index b5ba89d36..59b06626b 100644 --- a/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx +++ b/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx @@ -53,6 +53,7 @@ import { getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, type PlatformResourceStatusFilter, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { useColumnVisibility } from '@/hooks/useColumnVisibility'; import type { Disk } from '@/types/api'; @@ -1411,7 +1412,10 @@ export const AgentsMachinesTable: Component<{ searchTips={AGENT_MACHINE_SEARCH_TIPS} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="machines" diff --git a/frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx b/frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx index 4a7777016..d516a0c45 100644 --- a/frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx +++ b/frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx @@ -18,6 +18,7 @@ import { getPlatformTableHeadClassForKind, type PlatformResourceStatusFilter, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailTableRow, @@ -112,7 +113,10 @@ export const AvailabilityChecksTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="checks" diff --git a/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx b/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx index aa5302eb1..f47bc317d 100644 --- a/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx @@ -10,6 +10,7 @@ import { getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -142,7 +143,10 @@ export const TrueNASAlertsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={TRUENAS_INCIDENT_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + TRUENAS_INCIDENT_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="alerts" diff --git a/frontend-modern/src/features/truenas/TrueNASAppsTable.tsx b/frontend-modern/src/features/truenas/TrueNASAppsTable.tsx index d361be71b..4c8a7cb81 100644 --- a/frontend-modern/src/features/truenas/TrueNASAppsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASAppsTable.tsx @@ -23,6 +23,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -220,7 +221,10 @@ export const TrueNASAppsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={TRUENAS_APP_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + TRUENAS_APP_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="apps" diff --git a/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx b/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx index bea7440e5..71246ce71 100644 --- a/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx @@ -16,6 +16,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -162,7 +163,10 @@ export const TrueNASNetworkSharesTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={TRUENAS_SHARE_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + TRUENAS_SHARE_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="shares" diff --git a/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx b/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx index d4d6a0fa0..193d78bda 100644 --- a/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx @@ -22,7 +22,7 @@ import { formatPlatformTableBytesValue, getPlatformTableCellClassForKind, getPlatformTableDateTimeSortValue, - withPlatformAttentionCount, + withPlatformStatusCounts, type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, @@ -357,20 +357,18 @@ export const TrueNASProtectionTable: Component<{ }> = (props) => { const rows = createMemo(() => sortTrueNASProtectionPoints(props.points)); const posture = createMemo(() => buildTrueNASProtectionPosture(rows())); - const statusOptions = createMemo(() => - withPlatformAttentionCount(TRUENAS_PROTECTION_STATUS_OPTIONS, { - value: 'attention', - count: posture().attention, - tone: posture().failed > 0 ? 'danger' : 'warning', - noun: 'protection issue', - }), - ); const detail = createPlatformResourceDetailState({ idPrefix: 'truenas-protection-detail' }); const tableState = createPlatformTableFilterState({ resources: rows, initialStatus: 'all' as TrueNASProtectionStatusFilter, filter: filterTrueNASProtectionPoints, }); + const statusOptions = createMemo(() => + withPlatformStatusCounts(TRUENAS_PROTECTION_STATUS_OPTIONS, tableState.countForStatus, { + value: 'attention', + tone: posture().failed > 0 ? 'danger' : 'warning', + }), + ); const sort = createPlatformTableSortState({ storageKey: 'truenasProtection', sortKeys: TRUENAS_PROTECTION_SORT_KEYS, diff --git a/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx b/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx index c657f34c1..60dfe928f 100644 --- a/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASServicesTable.tsx @@ -14,6 +14,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -226,7 +227,10 @@ export const TrueNASServicesTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={TRUENAS_SERVICE_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + TRUENAS_SERVICE_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="services" diff --git a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx index e7572f3a3..01cfa749c 100644 --- a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx @@ -22,6 +22,7 @@ import { type PlatformTableSortState, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -317,7 +318,10 @@ export const TrueNASStorageTopologyTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={TRUENAS_STORAGE_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + TRUENAS_STORAGE_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="items" diff --git a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx index 8615fdff5..0859cf661 100644 --- a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx @@ -30,6 +30,7 @@ import { type PlatformResourceStatusFilter, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -189,7 +190,10 @@ export const TrueNASSystemsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="systems" diff --git a/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx b/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx index f5c6411cb..4a3b809df 100644 --- a/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx @@ -16,6 +16,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -184,7 +185,10 @@ export const TrueNASVirtualMachinesTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={TRUENAS_VM_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + TRUENAS_VM_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="VMs" diff --git a/frontend-modern/src/features/truenas/__tests__/TrueNASProtectionTable.test.tsx b/frontend-modern/src/features/truenas/__tests__/TrueNASProtectionTable.test.tsx index 2644b9394..d343b467a 100644 --- a/frontend-modern/src/features/truenas/__tests__/TrueNASProtectionTable.test.tsx +++ b/frontend-modern/src/features/truenas/__tests__/TrueNASProtectionTable.test.tsx @@ -50,7 +50,7 @@ describe('TrueNASProtectionTable', () => { ); const attentionFilter = screen.getByRole('button', { - name: 'Attention, 1 protection issue', + name: 'Attention, 1', }); expect(attentionFilter).toHaveTextContent('Attention'); expect(attentionFilter).toHaveTextContent('1'); @@ -59,7 +59,7 @@ describe('TrueNASProtectionTable', () => { expect(document.querySelectorAll('[data-truenas-protection-row]')).toHaveLength(1); expect(document.querySelector('[data-truenas-protection-row="failed"]')).not.toBeNull(); - await fireEvent.click(screen.getByRole('button', { name: 'All' })); + await fireEvent.click(screen.getByRole('button', { name: 'All, 3' })); expect(document.querySelectorAll('[data-truenas-protection-row]')).toHaveLength(3); }); diff --git a/frontend-modern/src/features/truenas/__tests__/TrueNASSystemsTable.test.tsx b/frontend-modern/src/features/truenas/__tests__/TrueNASSystemsTable.test.tsx index 6bf623fa0..401b95b28 100644 --- a/frontend-modern/src/features/truenas/__tests__/TrueNASSystemsTable.test.tsx +++ b/frontend-modern/src/features/truenas/__tests__/TrueNASSystemsTable.test.tsx @@ -99,7 +99,7 @@ describe('TrueNASSystemsTable', () => { await fireEvent.click( within(screen.getByRole('group', { name: 'Status' })).getByRole('button', { - name: 'Degraded', + name: /^Degraded, \d+$/, }), ); diff --git a/frontend-modern/src/features/vmware/VsphereActivityTable.tsx b/frontend-modern/src/features/vmware/VsphereActivityTable.tsx index ee8cc3e03..8d59aa01f 100644 --- a/frontend-modern/src/features/vmware/VsphereActivityTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereActivityTable.tsx @@ -23,6 +23,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -281,7 +282,10 @@ export const VsphereActivityTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={VSPHERE_ACTIVITY_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + VSPHERE_ACTIVITY_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="events" diff --git a/frontend-modern/src/features/vmware/VsphereAlertsTable.tsx b/frontend-modern/src/features/vmware/VsphereAlertsTable.tsx index 3169574c5..c108ab353 100644 --- a/frontend-modern/src/features/vmware/VsphereAlertsTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereAlertsTable.tsx @@ -17,7 +17,7 @@ import { getPlatformTableCellClassForKind, getPlatformTableHeadClassForKind, PlatformTableShell, - withPlatformAttentionCount, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -124,11 +124,9 @@ export const VsphereAlertsTable: Component<{ const drawer = createPlatformResourceDetailState({ idPrefix: 'vsphere-alert-drawer' }); const posture = createMemo(() => buildVmwareHealthPosture(props.incidents)); const statusOptions = createMemo(() => - withPlatformAttentionCount(VSPHERE_INCIDENT_STATUS_OPTIONS, { + withPlatformStatusCounts(VSPHERE_INCIDENT_STATUS_OPTIONS, tableState.countForStatus, { value: 'attention', - count: posture().attention, tone: posture().critical > 0 ? 'danger' : 'warning', - noun: 'health signal', }), ); const filteredEmptyState = () => getAlertFilteredEmptyState('vSphere health signals', 'severity'); diff --git a/frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx b/frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx index 08f3ca833..e13a75e4f 100644 --- a/frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx @@ -19,6 +19,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -173,7 +174,10 @@ export const VsphereDatastoresTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={VSPHERE_DATASTORE_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + VSPHERE_DATASTORE_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="datastores" diff --git a/frontend-modern/src/features/vmware/VsphereHostsTable.tsx b/frontend-modern/src/features/vmware/VsphereHostsTable.tsx index 061027ead..380ab5878 100644 --- a/frontend-modern/src/features/vmware/VsphereHostsTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereHostsTable.tsx @@ -32,6 +32,7 @@ import { getPlatformTableCellClassForKind, type PlatformResourceStatusFilter, type PlatformTableSortValue, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -178,7 +179,10 @@ export const VsphereHostsTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={PLATFORM_HEALTH_FILTER_OPTIONS} + statusOptions={withPlatformStatusCounts( + PLATFORM_HEALTH_FILTER_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="hosts" diff --git a/frontend-modern/src/features/vmware/VsphereNetworksTable.tsx b/frontend-modern/src/features/vmware/VsphereNetworksTable.tsx index 91c978c67..94d88709b 100644 --- a/frontend-modern/src/features/vmware/VsphereNetworksTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereNetworksTable.tsx @@ -16,6 +16,7 @@ import { type PlatformTableFilterOption, type PlatformTableSortValue, PlatformTableShell, + withPlatformStatusCounts, } from '@/features/platformPage/sharedPlatformPage'; import { PlatformResourceDetailToggleButton, @@ -149,7 +150,10 @@ export const VsphereNetworksTable: Component<{ searchSuggestions={tableState.searchSuggestions} status={tableState.status()} onStatusChange={tableState.setStatus} - statusOptions={VSPHERE_NETWORK_STATUS_OPTIONS} + statusOptions={withPlatformStatusCounts( + VSPHERE_NETWORK_STATUS_OPTIONS, + tableState.countForStatus, + )} visible={tableState.visible()} total={tableState.total()} rowNoun="networks" diff --git a/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx b/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx index 3725d3179..16a86d69b 100644 --- a/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx +++ b/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx @@ -92,14 +92,14 @@ describe('VsphereAlertsTable', () => { expect(within(firstRow).getByText('host-101')).toBeInTheDocument(); const attentionFilter = screen.getByRole('button', { - name: 'Attention, 1 health signal', + name: 'Attention, 1', }); expect(attentionFilter).toHaveTextContent('Attention'); expect(attentionFilter).toHaveTextContent('1'); await fireEvent.click(attentionFilter); expect(document.querySelectorAll('[data-vsphere-alert-row]')).toHaveLength(1); - await fireEvent.click(screen.getByRole('button', { name: 'All' })); + await fireEvent.click(screen.getByRole('button', { name: /^All, \d+$/ })); expect(document.querySelectorAll('[data-vsphere-alert-row]')).toHaveLength(2); const row = screen diff --git a/frontend-modern/src/features/vmware/__tests__/VsphereHostsTable.test.tsx b/frontend-modern/src/features/vmware/__tests__/VsphereHostsTable.test.tsx index 975c1ddc0..60a08db6c 100644 --- a/frontend-modern/src/features/vmware/__tests__/VsphereHostsTable.test.tsx +++ b/frontend-modern/src/features/vmware/__tests__/VsphereHostsTable.test.tsx @@ -102,7 +102,7 @@ describe('VsphereHostsTable', () => { await fireEvent.click( within(screen.getByRole('group', { name: 'Status' })).getByRole('button', { - name: 'Degraded', + name: /^Degraded, \d+$/, }), );