From ee4cd560b6352fac50b988104e3ee370db8669c1 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 20 Aug 2026 21:52:34 +0100 Subject: [PATCH] Count platform status chips from each table's own filter predicate Only two platform tables ever adopted the attention-count affordance, so Docker, Kubernetes, TrueNAS, Machines, and the Proxmox detail tables all rendered bare status chips while the Proxmox and vSphere workloads bars showed counts. Replace withPlatformAttentionCount with withPlatformStatusCounts and roll it across every PlatformTableToolbar status facet: each chip now carries the row count its own selection would render, sourced from the same predicate the table filters with (createPlatformTableFilterState.countForStatus, or the surface's governing filter on the bespoke Docker storage, Kubernetes section, Ceph and Replication toolbars), so a chip's number can never disagree with the rows clicking it shows. Counts respect the existing estate-wide 'Inventory totals' visibility preference, the attention tables keep their tone escalation, and the container-attachment options drop their static ariaLabels so the count is part of the accessible name. --- frontend-modern/browser-verification.json | 134 +++++++++++++++--- .../SharedPrimitives.guardrails.test.ts | 8 +- .../src/features/docker/DockerAlertsTable.tsx | 6 +- .../features/docker/DockerConfigsTable.tsx | 6 +- .../features/docker/DockerContainersTable.tsx | 15 +- .../src/features/docker/DockerHostsTable.tsx | 6 +- .../src/features/docker/DockerImagesTable.tsx | 6 +- .../features/docker/DockerNetworksTable.tsx | 18 ++- .../src/features/docker/DockerPageSurface.tsx | 14 +- .../features/docker/DockerSecretsTable.tsx | 6 +- .../features/docker/DockerServicesTable.tsx | 6 +- .../docker/DockerStorageUsageTable.tsx | 6 +- .../features/docker/DockerSwarmNodesTable.tsx | 6 +- .../src/features/docker/DockerTasksTable.tsx | 6 +- .../features/docker/DockerVolumesTable.tsx | 6 +- .../__tests__/DockerNativeTables.test.tsx | 6 +- .../__tests__/DockerPageSurface.test.tsx | 2 +- .../kubernetes/KubernetesAlertsTable.tsx | 6 +- .../kubernetes/KubernetesAutoscalingTable.tsx | 6 +- .../kubernetes/KubernetesClustersTable.tsx | 6 +- .../kubernetes/KubernetesConfigTable.tsx | 6 +- .../kubernetes/KubernetesControllersTable.tsx | 6 +- .../kubernetes/KubernetesDeploymentsTable.tsx | 6 +- .../kubernetes/KubernetesEventsTable.tsx | 6 +- .../kubernetes/KubernetesNetworkingTable.tsx | 6 +- .../kubernetes/KubernetesNodesTable.tsx | 6 +- .../kubernetes/KubernetesPageSurface.tsx | 25 ++-- .../kubernetes/KubernetesPodsTable.tsx | 6 +- .../kubernetes/KubernetesPolicyTable.tsx | 6 +- .../kubernetes/KubernetesServicesTable.tsx | 6 +- .../kubernetes/KubernetesStorageTable.tsx | 6 +- .../platformOverviewLayout.guardrails.test.ts | 2 +- .../__tests__/sharedPlatformPage.test.ts | 66 ++++----- .../platformPage/sharedPlatformPage.tsx | 53 +++---- .../src/features/proxmox/ProxmoxCephTable.tsx | 10 +- .../proxmox/ProxmoxMailGatewayTable.tsx | 6 +- .../proxmox/ProxmoxReplicationTable.tsx | 10 +- .../standalone/AgentsMachinesTable.tsx | 6 +- .../standalone/AvailabilityChecksTable.tsx | 6 +- .../features/truenas/TrueNASAlertsTable.tsx | 6 +- .../src/features/truenas/TrueNASAppsTable.tsx | 6 +- .../truenas/TrueNASNetworkSharesTable.tsx | 6 +- .../truenas/TrueNASProtectionTable.tsx | 16 +-- .../features/truenas/TrueNASServicesTable.tsx | 6 +- .../truenas/TrueNASStorageTopologyTable.tsx | 6 +- .../features/truenas/TrueNASSystemsTable.tsx | 6 +- .../truenas/TrueNASVirtualMachinesTable.tsx | 6 +- .../__tests__/TrueNASProtectionTable.test.tsx | 4 +- .../__tests__/TrueNASSystemsTable.test.tsx | 2 +- .../features/vmware/VsphereActivityTable.tsx | 6 +- .../features/vmware/VsphereAlertsTable.tsx | 6 +- .../vmware/VsphereDatastoresTable.tsx | 6 +- .../src/features/vmware/VsphereHostsTable.tsx | 6 +- .../features/vmware/VsphereNetworksTable.tsx | 6 +- .../__tests__/VsphereAlertsTable.test.tsx | 4 +- .../__tests__/VsphereHostsTable.test.tsx | 2 +- 56 files changed, 441 insertions(+), 178 deletions(-) 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+$/, }), );