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.
This commit is contained in:
rcourtman
2026-08-20 21:52:34 +01:00
parent df1290066e
commit ee4cd560b6
56 changed files with 441 additions and 178 deletions
+115 -19
View File
@@ -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"
]
}
@@ -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('<MetadataBadge');
expect(sharedPlatformPageSource).toContain('export function withPlatformStatusCounts');
expect(sharedPlatformPageSource).toContain(
'count: inventoryCountsVisible() ? option.count : undefined',
);
for (const source of [truenasProtectionTableSource, vsphereAlertsTableSource]) {
expect(source).toContain('withPlatformAttentionCount');
expect(source).toContain('withPlatformStatusCounts');
expect(source).not.toContain('PlatformAttentionSummary');
expect(source).not.toContain('<MetadataBadge');
}
@@ -16,6 +16,7 @@ import {
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
PlatformTableShell,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -138,7 +139,10 @@ export const DockerAlertsTable: Component<{
searchSuggestions={tableState.searchSuggestions}
status={tableState.status()}
onStatusChange={tableState.setStatus}
statusOptions={DOCKER_INCIDENT_STATUS_OPTIONS}
statusOptions={withPlatformStatusCounts(
DOCKER_INCIDENT_STATUS_OPTIONS,
tableState.countForStatus,
)}
visible={tableState.visible()}
total={tableState.total()}
rowNoun="alerts"
@@ -13,6 +13,7 @@ import {
getPlatformTableDateTimeSortValue,
PlatformTableShell,
type PlatformTableSortValue,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -92,7 +93,10 @@ export const DockerConfigsTable: Component<DockerNativeTableProps> = (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"
@@ -43,6 +43,7 @@ import {
getPlatformTableCellClassForKind,
PlatformTableShell,
type PlatformTableSortValue,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -346,11 +347,15 @@ export const DockerContainersTable: Component<DockerContainersTableProps> = (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<DockerContainersTableProps> = (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()}
@@ -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"
@@ -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"
@@ -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<AttachmentStatusFilter>[] = [
{ 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<DockerNetworksTableProps> = (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"
@@ -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"
@@ -13,6 +13,7 @@ import {
getPlatformTableDateTimeSortValue,
PlatformTableShell,
type PlatformTableSortValue,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -94,7 +95,10 @@ export const DockerSecretsTable: Component<DockerNativeTableProps> = (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"
@@ -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"
@@ -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"
@@ -13,6 +13,7 @@ import {
getPlatformTableCellClassForKind,
PlatformTableShell,
type PlatformTableSortValue,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -125,7 +126,10 @@ export const DockerSwarmNodesTable: Component<DockerNativeTableProps> = (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"
@@ -13,6 +13,7 @@ import {
getPlatformTableDateTimeSortValue,
PlatformTableShell,
type PlatformTableSortValue,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -108,7 +109,10 @@ export const DockerTasksTable: Component<DockerNativeTableProps> = (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"
@@ -14,6 +14,7 @@ import {
getPlatformTableDateTimeSortValue,
PlatformTableShell,
type PlatformTableSortValue,
withPlatformStatusCounts,
} from '@/features/platformPage/sharedPlatformPage';
import {
PlatformResourceDetailToggleButton,
@@ -111,7 +112,10 @@ export const DockerVolumesTable: Component<DockerNativeTableProps> = (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"
@@ -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' },
});
@@ -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(
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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={
<Show when={(props.attentionCount ?? 0) > 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()}
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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) {
@@ -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<typeof withPlatformAttentionCount<string>>[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<string, number> = { 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);
});
});
@@ -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<T extends string | number>(
// 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<T extends string | number>(
options: readonly PlatformTableFilterOption<T>[],
config: {
value: T;
count: number;
tone: 'warning' | 'danger';
noun: string;
},
countForStatus: (value: T) => number,
attention?: { value: T; tone: 'warning' | 'danger' },
): PlatformTableFilterOption<T>[] {
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: (
<>
<span>{option.label}</span>
<MetadataBadge tone={config.tone} size="xs" aria-hidden="true">
{count}
</MetadataBadge>
</>
),
count,
...(escalate ? { tone: attention.tone } : {}),
};
});
}
@@ -1154,6 +1143,11 @@ export function createPlatformTableFilterState<Row, Status extends string | numb
const filtered = createMemo(() => 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<Row, Status extends string | numb
filtered,
visible,
total,
countForStatus,
hasActiveFilters,
searchSuggestions,
resetFilters,
@@ -1297,6 +1292,9 @@ export function PlatformTableToolbar<T extends string | number>(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"
@@ -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"
@@ -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"
@@ -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<ReplicationStatusFilter>('all');
const [expandedJobKey, setExpandedJobKey] = createSignal<string | null>(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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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,
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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);
});
@@ -99,7 +99,7 @@ describe('TrueNASSystemsTable', () => {
await fireEvent.click(
within(screen.getByRole('group', { name: 'Status' })).getByRole('button', {
name: 'Degraded',
name: /^Degraded, \d+$/,
}),
);
@@ -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"
@@ -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');
@@ -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"
@@ -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"
@@ -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"
@@ -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
@@ -102,7 +102,7 @@ describe('VsphereHostsTable', () => {
await fireEvent.click(
within(screen.getByRole('group', { name: 'Status' })).getByRole('button', {
name: 'Degraded',
name: /^Degraded, \d+$/,
}),
);