From 5ed053dfc20b660f9f3ea4e871e4a2209e2f7e82 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 20 Aug 2026 22:08:43 +0100 Subject: [PATCH] Count the alert history severity chips from the list's own predicate The alert history FilterBar rendered bare Critical/Warning chips while the platform tables' status chips now carry counts. Expose countForSeverity from useAlertHistoryState, built on the same filterAlertHistoryItems predicate the list renders through, so each severity chip shows the row count its selection yields for the fetched period and current search. Counts respect the shared Inventory totals visibility preference; the Period facet stays uncounted because it is a time scope, and only the currently fetched range is available to count. --- frontend-modern/browser-verification.json | 127 ++---------------- .../alerts/AlertHistoryFiltersCard.tsx | 16 ++- .../__tests__/useAlertHistoryState.test.tsx | 42 ++++++ .../features/alerts/useAlertHistoryState.ts | 7 + 4 files changed, 76 insertions(+), 116 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 3cb0a34b9..df748f40b 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,112 +1,18 @@ { "version": 1, - "base_sha": "df1290066e2907b344b9294d0106dcbabde03a53", - "verified_at": "2026-08-20T20:51:48Z", + "base_sha": "ee4cd560b6352fac50b988104e3ee370db8669c1", + "verified_at": "2026-08-20T21:08:35Z", "result": "passed", "changed_paths": [ - "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" + "frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx", + "frontend-modern/src/features/alerts/useAlertHistoryState.ts" ], "content_sha256": { - "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" + "frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx": "07836c5b85ea484cfe984b2d0de5c302202ef6b844eafdc776b3f979f0938acb", + "frontend-modern/src/features/alerts/useAlertHistoryState.ts": "7528bd735ff84961e8bd880d89f0253255c698ab5910c1eb2fb10cf0255e39e8" }, "routes": [ - "/docker/overview", - "/kubernetes/workloads", - "/truenas (via nav tab)", - "/standalone/machines (via nav tab)", - "/vmware/overview (via nav tab)", - "/proxmox/replication", - "/proxmox/ceph" + "/alerts/history" ], "viewports": [ { @@ -119,21 +25,12 @@ } ], "states": [ - "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" + "Alert history severity facet now counted from the live history data: All 47 / Critical 8 / Warning 13, with Period chips left uncounted as time scopes", + "Critical selected: the page's own summary reads '8 alerts', matching the chip count exactly (counts share filterAlertHistoryItems with the list)", + "Chips verified through the mobile Filters sheet (the hidden preview pane renders the responsive mobile layout); the same options render inline at desktop width" ], "interactions": [ - "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" + "Opened the Filters sheet on /alerts/history and read the counted severity chips", + "Clicked the Critical chip, confirmed the '8 alerts' summary, then reset the facet back to All" ] } diff --git a/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx b/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx index 888ccf66f..daf772bba 100644 --- a/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx +++ b/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx @@ -1,4 +1,9 @@ import { FilterBar, filterChipStatusDot, type FilterDef } from '@/components/shared/FilterBar'; +import { + PLATFORM_ESTATE_COUNTS_STORAGE_KEY, + deserializePlatformEstateCountsVisibility, +} from '@/features/platformPage/platformEstateOverviewModel'; +import { usePersistentSignal } from '@/hooks/usePersistentSignal'; import { STORAGE_KEYS } from '@/utils/localStorage'; import { ALERT_HISTORY_ALL_TIME_FILTER_LABEL, @@ -13,6 +18,13 @@ interface AlertHistoryFiltersCardProps { } export function AlertHistoryFiltersCard(props: AlertHistoryFiltersCardProps) { + // Same estate-wide preference that gates the platform tables' chip counts, + // so hiding inventory totals quiets this facet too. + const [countsVisible] = usePersistentSignal(PLATFORM_ESTATE_COUNTS_STORAGE_KEY, true, { + deserialize: deserializePlatformEstateCountsVisibility, + }); + const severityCount = (value: AlertSeverityFilter): number | undefined => + countsVisible() ? props.state.countForSeverity(value) : undefined; const buildFilters = (): FilterDef[] => [ { id: 'alert-period', @@ -38,18 +50,20 @@ export function AlertHistoryFiltersCard(props: AlertHistoryFiltersCardProps) { setValue: (value: string) => props.state.setSeverityFilter(value as AlertSeverityFilter), defaultValue: 'all', options: () => [ - { value: 'all', label: 'All' }, + { value: 'all', label: 'All', count: severityCount('all') }, { value: 'critical', label: 'Critical', leading: filterChipStatusDot('bg-red-500'), tone: 'danger', + count: severityCount('critical'), }, { value: 'warning', label: 'Warning', leading: filterChipStatusDot('bg-amber-500'), tone: 'warning', + count: severityCount('warning'), }, ], }, diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx index d2cf9fc6d..a374ae5eb 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx @@ -156,6 +156,48 @@ describe('useAlertHistoryState', () => { expect(result.alertHistory()).toEqual([]); }); + it('counts each severity chip from the same predicate the list filters with', async () => { + const [activeAlerts] = createSignal({}); + const now = Date.now(); + const startTime = new Date(now - 30 * 60 * 1000).toISOString(); + const makeEntry = (id: string, level: string, resourceName: string) => ({ + id, + type: 'cpu', + level, + startTime, + lastSeen: startTime, + resourceId: `resource-${id}`, + resourceName, + message: 'CPU high', + acknowledged: false, + }); + vi.mocked(AlertsAPI.getHistory).mockResolvedValue([ + makeEntry('alert-1', 'critical', 'db-01'), + makeEntry('alert-2', 'warning', 'db-01'), + makeEntry('alert-3', 'warning', 'web-01'), + ] as any); + + const { result } = renderHook(() => + useAlertHistoryState({ + activeAlerts, + getResource: () => undefined, + allResources: () => [], + }), + ); + + await waitFor(() => expect(result.countForSeverity('all')).toBe(3)); + expect(result.countForSeverity('critical')).toBe(1); + expect(result.countForSeverity('warning')).toBe(2); + + // Counts ignore the selected severity (each chip shows what its own + // selection would render) but follow the search term. + result.setSeverityFilter('critical'); + expect(result.countForSeverity('warning')).toBe(2); + result.setSearchTerm('db-01'); + await waitFor(() => expect(result.countForSeverity('warning')).toBe(1)); + expect(result.countForSeverity('all')).toBe(2); + }); + it('clears search, period, and severity in one route write', async () => { const [activeAlerts] = createSignal({}); vi.mocked(AlertsAPI.getHistory).mockResolvedValue([] as any); diff --git a/frontend-modern/src/features/alerts/useAlertHistoryState.ts b/frontend-modern/src/features/alerts/useAlertHistoryState.ts index d9926c9ec..1afd4d7c2 100644 --- a/frontend-modern/src/features/alerts/useAlertHistoryState.ts +++ b/frontend-modern/src/features/alerts/useAlertHistoryState.ts @@ -269,6 +269,12 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) { return filterAlertHistoryItems(allHistoryData(), severityFilter(), searchTerm()); }); + // Chip-count source for the severity facet: the same predicate the list + // filters with, so each chip shows the row count its selection renders + // (before any chart bar drill-down, which narrows further and explicitly). + const countForSeverity = (value: AlertSeverityFilter): number => + filterAlertHistoryItems(allHistoryData(), value, searchTerm()).length; + const alertTrends = createMemo(() => { return buildAlertTrends(severityAndSearchFilteredItems(), timeFilter()); }); @@ -334,6 +340,7 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) { resourceIncidentEventFilters: resourceIncidentsState.resourceIncidentEventFilters, setResourceIncidentEventFilters: resourceIncidentsState.setResourceIncidentEventFilters, activeFilterCount, + countForSeverity, clearFilters, incidentTimelines, incidentLoading,