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.
This commit is contained in:
rcourtman
2026-08-20 22:08:43 +01:00
parent ee4cd560b6
commit 5ed053dfc2
4 changed files with 76 additions and 116 deletions
+12 -115
View File
@@ -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"
]
}
@@ -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'),
},
],
},
@@ -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);
@@ -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,