Separate threshold platform navigation from filters

This commit is contained in:
rcourtman
2026-08-03 08:54:14 +01:00
parent 1b2e9866a8
commit c35ab1ad1c
4 changed files with 90 additions and 60 deletions
@@ -430,9 +430,12 @@ perform notification work. Canonical lifecycle and stateful cooldown refires
consume resolved state through the shared lock-order-aware helper, preserve the
original alert `StartTime`, and keep the five-minute refire/history semantics.
The browser thresholds surface is also platform-shaped: Proxmox, Docker,
Kubernetes, TrueNAS, vSphere, PBS, PMG, and Systems. It must use the shared
FilterBar chip and "+ Filter" pattern for resource filtering, and alert tables
must use the canonical platform table column-kind alignment helpers from
Kubernetes, TrueNAS, vSphere, PBS, PMG, and Systems. Route-backed platform
choices must use the shared `Subtabs` navigation above the shared `FilterBar`;
they are not resource-filter facets and must remain visible when the mobile
filter shell is collapsed. Resource filtering must use the shared FilterBar
chip and "+ Filter" pattern, and alert tables must use the canonical platform
table column-kind alignment helpers from
`frontend-modern/src/features/platformPage/` rather than hard-coded table
alignment classes.
Alert filter option semantics stay alert-owned, but FilterBar chip
@@ -1021,6 +1024,10 @@ links like `/alerts/thresholds/infrastructure`,
`/alerts/thresholds/containers`, and `/alerts/thresholds/mail-gateway` must
redirect to the matching platform-shaped route. Legacy
`/alerts/thresholds/agents` links must continue to resolve to Systems.
`frontend-modern/src/components/Alerts/ThresholdsTable.tsx` must expose those
peer platform routes through the shared `Subtabs` primitive rather than an
inline FilterBar group. Clearing search or override filters must preserve the
active platform route.
Within the Proxmox tab, render-heavy ownership now further routes through
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx`,
`frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx`,
+18 -23
View File
@@ -1,21 +1,18 @@
{
"version": 1,
"base_sha": "b2e5ad606176c0e08185e7da95b33a4ae4e39318",
"verified_at": "2026-08-03T00:21:48Z",
"base_sha": "1b2e9866a843386916fe58a9f73be1700ca6f7d6",
"verified_at": "2026-08-03T07:52:17Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Storage/StoragePageControls.tsx",
"frontend-modern/src/components/shared/FilterBar/FilterBar.tsx",
"frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx"
"frontend-modern/src/components/Alerts/ThresholdsTable.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Storage/StoragePageControls.tsx": "f3ea9419ff3b7de9f21deba51add5485e15751ec58842eee532fcf30e9d28782",
"frontend-modern/src/components/shared/FilterBar/FilterBar.tsx": "97c31217224436d7326b34629ad26d7e8fbafa9c50033a2faaf9877af49e3d9d",
"frontend-modern/src/components/shared/FilterBar/ViewOptionsMenu.tsx": "a4af7c1e398bab745109d332dff808f273b85067e80cf5c53c11abf6f1fe2f98"
"frontend-modern/src/components/Alerts/ThresholdsTable.tsx": "3c2eb5f190d916a16ff9f03e247939726e36957f0ebd5f8b8f0647fb57d99be1"
},
"routes": [
"/proxmox/storage",
"/docker/overview"
"/alerts/thresholds/proxmox",
"/alerts/thresholds/docker",
"/alerts/thresholds/systems"
],
"viewports": [
{
@@ -28,20 +25,18 @@
}
],
"states": [
"Desktop Proxmox Storage toolbar with Status as the only permanent segmented filter and Saved, Clear filters, and View aligned on the shared rail",
"Desktop Storage View preferences with Layout/Group by and Order controls fully visible",
"Phone-width Storage filter rail expanded with Saved, Clear filters, and View together in the labelled Filter actions row",
"Phone-width Storage View preferences fully contained within the viewport",
"Phone-width Docker filter rail expanded with Saved, View, and the container count in the shared action row",
"Phone-width Docker View preferences fully contained despite the trailing container count"
"Desktop Proxmox threshold page with the route-backed platform rail above the resource FilterBar",
"Desktop Docker threshold page with Docker selected and only Overrides available from Add filter",
"Phone-width Docker threshold page with Filters collapsed and the platform rail still visible",
"Phone-width Systems threshold page after the platform rail scrolled Machines into view",
"Phone-width Systems resource filters expanded with only the Overrides choices present"
],
"interactions": [
"Changed Storage Group by from Flat to By node and confirmed grouped host rows plus the canonical group=node URL parameter",
"Reloaded Storage and confirmed the By node selection and grouped result persisted from URL state",
"Pressed Escape and confirmed the View dialog closed with focus restored to its trigger",
"Reopened View and clicked the Storage tab outside it, confirming outside-click dismissal",
"Expanded the phone-width Storage filter rail and confirmed Group by no longer consumes an inline filter row",
"Opened View on phone-width Storage and Docker routes and confirmed the action-rail-relative panel stayed inside both viewport edges",
"Confirmed the verified routes produced no browser warning or error console entries"
"Selected Docker from the threshold platform rail and confirmed navigation to the canonical Docker threshold route",
"Applied Overrides: Custom only, cleared filters, and confirmed Docker remained selected with no route change",
"Reloaded at phone width with Filters collapsed and confirmed platform navigation remained available",
"Selected Machines from the horizontally scrollable phone-width platform rail and confirmed navigation to Systems",
"Expanded and collapsed phone-width Filters and confirmed the platform choices were never duplicated as filter options",
"Confirmed the phone-width document had no horizontal overflow and the final verified routes produced no browser warning or error console entries"
]
}
@@ -1,6 +1,7 @@
import { Show } from 'solid-js';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { FilterBar, type FilterDef } from '@/components/shared/FilterBar';
import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs';
import { useBreakpoint } from '@/hooks/useBreakpoint';
import { BulkEditDialog } from './BulkEditDialog';
import { ThresholdsTableAgentsTab } from './ThresholdsTableAgentsTab';
@@ -13,28 +14,33 @@ import type { ThresholdsTableProps } from '@/features/alerts/thresholds/types';
import type { ThresholdsActiveTab } from '@/features/alerts/thresholds/tableTypes';
import { useThresholdsTableState } from '@/features/alerts/thresholds/hooks/useThresholdsTableState';
const thresholdPlatformDefinitions = [
{ value: 'proxmox', label: 'Proxmox', icon: getPlatformIcon('proxmox') },
{ value: 'docker', label: 'Docker', icon: getPlatformIcon('docker') },
{ value: 'kubernetes', label: 'Kubernetes', icon: getPlatformIcon('kubernetes') },
{ value: 'truenas', label: 'TrueNAS', icon: getPlatformIcon('truenas') },
{ value: 'vmware', label: 'vSphere', icon: getPlatformIcon('vmware') },
{ value: 'systems', label: 'Machines', icon: getPlatformIcon('systems') },
] as const;
export function ThresholdsTable(props: ThresholdsTableProps) {
const state = useThresholdsTableState(props);
const { isMobile } = useBreakpoint();
const platformTabs: SubtabOption[] = thresholdPlatformDefinitions.map(
({ value, label, icon: Icon }) => ({
value,
label: (
<span class="inline-flex items-center gap-2">
<span aria-hidden="true">
<Icon class="h-4 w-4" />
</span>
<span>{label}</span>
</span>
),
}),
);
const filters = (): FilterDef[] => [
{
id: 'alerts-platform',
label: 'Platform',
group: 'scope',
inline: true,
value: state.activeTab,
setValue: (value) => state.handleTabClick(value as ThresholdsActiveTab),
defaultValue: 'proxmox',
options: () => [
{ value: 'proxmox', label: 'Proxmox', icon: getPlatformIcon('proxmox') },
{ value: 'docker', label: 'Docker', icon: getPlatformIcon('docker') },
{ value: 'kubernetes', label: 'Kubernetes', icon: getPlatformIcon('kubernetes') },
{ value: 'truenas', label: 'TrueNAS', icon: getPlatformIcon('truenas') },
{ value: 'vmware', label: 'vSphere', icon: getPlatformIcon('vmware') },
{ value: 'systems', label: 'Machines', icon: getPlatformIcon('systems') },
],
},
{
id: 'alerts-overrides',
label: 'Overrides',
@@ -52,6 +58,13 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
return (
<div class="space-y-4">
<Subtabs
value={state.activeTab()}
onChange={(value) => state.handleTabClick(value as ThresholdsActiveTab)}
tabs={platformTabs}
ariaLabel="Threshold platform"
/>
<FilterBar
role="group"
ariaLabel="Alert threshold filters"
@@ -65,14 +78,11 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
}}
filters={filters()}
showClearAll={() =>
state.searchTerm().trim().length > 0 ||
state.overrideFilter() !== 'all' ||
state.activeTab() !== 'proxmox'
state.searchTerm().trim().length > 0 || state.overrideFilter() !== 'all'
}
onClearAll={() => {
state.setSearchTerm('');
state.setOverrideFilter('all');
state.handleTabClick('proxmox');
}}
/>
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, fireEvent, screen, cleanup, waitFor } from '@solidjs/testing-library';
import { render, fireEvent, screen, cleanup, waitFor, within } from '@solidjs/testing-library';
import { createSignal } from 'solid-js';
import { ThresholdsTable } from '../ThresholdsTable';
@@ -361,6 +361,18 @@ describe('ThresholdsTable basics', () => {
});
describe('ThresholdsTable navigation and redirection', () => {
it('renders route-backed platform choices as persistent navigation', () => {
render(() => <ThresholdsTable {...(baseProps() as any)} />);
const platformTabs = screen.getByRole('tablist', { name: 'Threshold platform' });
expect(within(platformTabs).getAllByRole('tab')).toHaveLength(6);
expect(within(platformTabs).getByRole('tab', { name: 'Docker' })).toHaveAttribute(
'aria-selected',
'true',
);
expect(screen.queryByText('Platform')).not.toBeInTheDocument();
});
it('redirects from base path to Proxmox', () => {
setPathname('/alerts/thresholds');
render(() => <ThresholdsTable {...(baseProps() as any)} />);
@@ -405,24 +417,30 @@ describe('ThresholdsTable navigation and redirection', () => {
it('navigates to correct route when tabs are clicked', () => {
render(() => <ThresholdsTable {...(baseProps() as any)} />);
const proxmoxTab = screen
.getAllByRole('button')
.find((el) => el.textContent?.includes('Proxmox'));
if (proxmoxTab) fireEvent.click(proxmoxTab);
const platformTabs = screen.getByRole('tablist', { name: 'Threshold platform' });
fireEvent.click(within(platformTabs).getByRole('tab', { name: 'Proxmox' }));
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/proxmox');
const machinesTab = screen
.getAllByRole('button')
.find((el) => el.textContent?.includes('Machines'));
if (machinesTab) fireEvent.click(machinesTab);
fireEvent.click(within(platformTabs).getByRole('tab', { name: 'Machines' }));
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/systems');
const vmwareTab = screen
.getAllByRole('button')
.find((el) => el.textContent?.includes('vSphere'));
if (vmwareTab) fireEvent.click(vmwareTab);
fireEvent.click(within(platformTabs).getByRole('tab', { name: 'vSphere' }));
expect(mockNavigate).toHaveBeenCalledWith('/alerts/thresholds/vmware');
});
it('clears resource filters without changing the active platform', () => {
render(() => <ThresholdsTable {...(baseProps() as any)} />);
const search = screen.getByPlaceholderText(/Search resources/i);
fireEvent.input(search, { target: { value: 'api' } });
mockNavigate.mockClear();
fireEvent.click(screen.getByRole('button', { name: 'Clear filters' }));
expect(search).toHaveValue('');
expect(mockNavigate).not.toHaveBeenCalled();
expect(screen.getByRole('tab', { name: 'Docker' })).toHaveAttribute('aria-selected', 'true');
});
});
describe('ThresholdsTable Resource Rendering', () => {