diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index c4c6b717de..76b7dc8e49 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -202,6 +202,9 @@ FilterBar chip and "+ Filter" pattern for resource filtering, 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 +presentation is frontend-primitives-owned: severity filter leading dots must +use `filterChipStatusDot` rather than alert-local span factories. Guest metric canonical state remains resource-backed and therefore node-scoped for Proxmox guests, so node moves must not strand active alert state on the previous resource ID. When a guest metric alert survives a node move, alerts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 952797fcdc..3b391d9282 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -84,6 +84,7 @@ work extends shared components instead of creating new local variants. 59. `frontend-modern/src/components/shared/FilterBar/index.ts` 59a. `frontend-modern/src/components/shared/FilterBar/SavedViewsMenu.tsx` 59b. `frontend-modern/src/components/shared/FilterBar/useSavedViews.ts` +59c. `frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx` 60. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` 61. `frontend-modern/src/features/` 62. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` @@ -2392,6 +2393,12 @@ Docker, Kubernetes, Proxmox, Standalone, TrueNAS, vSphere, and future platform feature tables; source-specific consumers own only the empty-state icon, title, description, and actions. Platform feature tables must not import `EmptyState` directly or recreate a `Card`-wrapped empty-state shell. +Filter bars are registry-backed too. `FilterBar` owns resource-list filtering +as a catalog of `FilterDef` entries, while `filterChipStatusDot` owns the +small leading status-dot glyph used by filter options. Page and feature +surfaces must not copy the chip-dot `` factory or import the legacy +`PageControls` deck for resource-list filtering; those drift checks live in +`shared-template-registry.json` and run through `shared-template-audit.mjs`. Inline detail table rows are also registry-backed. `InlineDetailTableRow` owns the row/cell/content shell and row-click containment for platform, workload, and infrastructure inline drawers; callers may pass row-specific @@ -2496,10 +2503,11 @@ rail as view options, matching the v5 filter-bar pattern, and keeps longer or dynamic scope filters in the menu/chip path. Feature surfaces must not fork local filter rows or bury high-frequency Type, Status, or Group-by filters behind an extra menu just to regain one-click behavior. -Pages that have not yet migrated (the alert-history filter card, -Kubernetes deployments drawer) keep using `PageControls` and -`LabeledFilterSelect`, but new resource-list filter surfaces should reach for -`FilterBar` with a catalog rather than reintroducing a per-page select row. +Legacy `PageControls` and labelled select/toggle primitives are not the +resource-list filter shape. If a future surface needs a new filtering +affordance, it should extend the FilterBar catalog model or add a new +registry-backed shared primitive rather than reintroducing a per-page select +row. Pages may opt into saved views by passing `savedViewsKey` to `FilterBar`. The `useSavedViews` hook owns the localStorage IO + URL diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 3835b1bfc0..c4d5071a45 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -374,6 +374,9 @@ regression protection. Non-Docker Workloads surfaces keep the global `disk` default unless they declare their own scoped contract. 18. Extend workload filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` (defaults, `countActiveWorkloadsFilters`, `hasActiveWorkloadsFilters`) rather than rebuilding filter-local state inside `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`. Workloads filter presentation now composes the shared `FilterBar` (`frontend-modern/src/components/shared/FilterBar/FilterBar.tsx`) with a per-page `FilterDef[]` catalog rather than the legacy `PageControls` structured control deck. High-frequency Type and Status filters stay in that catalog but render as inline compact segmented controls (`inline: true`), while longer or dynamic scope filters continue through the "+ Filter" menu and chip popovers. View options (grouped/list, charts, columns) sit in the shared `viewOptionsTrailing` slot. + Workload filter option semantics stay workload-owned, but FilterBar chip + presentation is frontend-primitives-owned: status and runtime leading dots + must use `filterChipStatusDot` rather than workload-local span factories. WorkloadsFilter remains the shared interaction shell only; platform-owned embedded Workloads surfaces must pass page-owned aria labels, search placeholders, empty-search copy, status labels, and forced view-mode diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 8352937e3f..8f9ae5d4f3 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -979,6 +979,9 @@ recovery scope, or a storage/recovery-owned secret source. forking the toolbar. The source scope flows through `forcedSourceFilter` as a typed page input; the source filter remains available in the toolbar only when not forced. + Storage filter option semantics stay storage-owned, but FilterBar chip + presentation is frontend-primitives-owned: storage status leading dots must + use `filterChipStatusDot` rather than storage-local span factories. 41. Keep agent memory composition descriptive on the shared unified-resource contract. `internal/unifiedresources/types.go` carries the reclaimable page-cache split (`AgentMemoryMeta.cache`, holding used + cache + free diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 627808d7f9..bb02f87f12 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -215,6 +215,10 @@ filter semantics, drawer handoffs, and resource projections, while Future platform tables must keep that split: row data and platform semantics stay in the unified-resource consumer, and the repeated table shell stays in the shared frontend primitive. +Platform filter option semantics follow that split too: unified-resource +consumers own the source-specific status buckets and labels, while the repeated +FilterBar chip leading-dot presentation must use the frontend-primitives-owned +`filterChipStatusDot` helper instead of page-local span factories. Platform table empty states use that same split. Unified-resource consumers own the source-specific empty-state vocabulary, action choice, and evidence rule that decides why a table is empty, but the table-card empty-state shell itself diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 4d37ade1dd..54c54ec44d 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -113,6 +113,60 @@ "src/components/shared/__tests__/InlineDetailTableRow.test.tsx", "scripts/shared-template-audit.mjs" ] + }, + { + "id": "filter-bar-catalog-shell", + "category": "filter-bar", + "summary": "Resource-list filter surfaces must compose the shared FilterBar catalog shell instead of recreating page-local filter rows or legacy PageControls decks.", + "canonical": { + "path": "src/components/shared/FilterBar/FilterBar.tsx", + "export": "FilterBar" + }, + "requiredConsumers": [ + { "path": "src/components/Alerts/ThresholdsTable.tsx" }, + { "path": "src/components/Settings/AuditLogPanel.tsx" }, + { "path": "src/components/Storage/StoragePageControls.tsx" }, + { "path": "src/components/Workloads/WorkloadsFilter.tsx" }, + { "path": "src/features/alerts/AlertHistoryFiltersCard.tsx" }, + { "path": "src/features/platformPage/sharedPlatformPage.tsx" }, + { "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" } + ], + "forbiddenPatterns": [ + { + "path": "src/components/Storage/StoragePageControls.tsx", + "patterns": [ + "from '@/components/shared/PageControls'", + " ( - -); - export const StoragePageControls: Component = (props) => { const { isMobile } = useBreakpoint(); const isPoolsView = () => props.view() === 'pools'; @@ -222,13 +218,13 @@ export const StoragePageControls: Component = (props) label: option.label, leading: option.value === 'available' - ? storageStatusDot('bg-emerald-500') + ? filterChipStatusDot('bg-emerald-500') : option.value === 'warning' || option.value === 'attention' - ? storageStatusDot('bg-amber-500') + ? filterChipStatusDot('bg-amber-500') : option.value === 'critical' - ? storageStatusDot('bg-red-500') + ? filterChipStatusDot('bg-red-500') : option.value === 'offline' || option.value === 'unknown' - ? storageStatusDot('bg-slate-400') + ? filterChipStatusDot('bg-slate-400') : undefined, tone: option.value === 'available' diff --git a/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx b/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx index ef13b1668e..7d9c7bb40c 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsFilter.tsx @@ -4,7 +4,12 @@ import BoxesIcon from 'lucide-solid/icons/boxes'; import MonitorIcon from 'lucide-solid/icons/monitor'; import XIcon from 'lucide-solid/icons/x'; import { ColumnPicker } from '@/components/shared/ColumnPicker'; -import { FilterBar, type FilterDef, type FilterSelectOption } from '@/components/shared/FilterBar'; +import { + FilterBar, + filterChipStatusDot, + type FilterDef, + type FilterSelectOption, +} from '@/components/shared/FilterBar'; import { ChartVisibilityToggleButton, FilterActionButton } from '@/components/shared/FilterToolbar'; import { GroupedTableModeSegmentedControl } from '@/components/shared/GroupedTableModeSegmentedControl'; import { useBreakpoint } from '@/hooks/useBreakpoint'; @@ -24,8 +29,6 @@ import { WORKLOAD_STATUS_FILTER_OPTIONS, WORKLOAD_TYPE_OPTIONS } from './workloa const PROXMOX_PLATFORM_FILTER = 'proxmox-pve'; -const statusDot = (className: string) => ; - export const WorkloadsFilter: Component = (props) => { const { isMobile } = useBreakpoint(); @@ -62,11 +65,11 @@ export const WorkloadsFilter: Component = (props) => { label: option.label, leading: option.value === 'running' - ? statusDot('bg-emerald-500') + ? filterChipStatusDot('bg-emerald-500') : option.value === 'degraded' - ? statusDot('bg-amber-500') + ? filterChipStatusDot('bg-amber-500') : option.value === 'stopped' - ? statusDot('bg-red-500') + ? filterChipStatusDot('bg-red-500') : undefined, tone: option.value === 'running' @@ -86,8 +89,8 @@ export const WorkloadsFilter: Component = (props) => { }; const runtimeChipDot = (value: string): JSX.Element | undefined => { - if (value === 'docker') return statusDot('bg-sky-500'); - if (value === 'podman') return statusDot('bg-violet-500'); + if (value === 'docker') return filterChipStatusDot('bg-sky-500'); + if (value === 'podman') return filterChipStatusDot('bg-violet-500'); return undefined; }; diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx index d3c6182a01..4a08bf86fd 100644 --- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, cleanup, fireEvent, within } from '@solidjs/testing-library'; import { WorkloadsFilter } from '../WorkloadsFilter'; +import workloadsFilterSource from '../WorkloadsFilter.tsx?raw'; import { DEFAULT_WORKLOADS_SORT_DIRECTION, DEFAULT_WORKLOADS_SORT_KEY, @@ -95,6 +96,12 @@ describe('WorkloadsFilter', () => { }); describe('rendering', () => { + it('uses the shared FilterBar status-dot presentation helper', () => { + expect(workloadsFilterSource).toContain('filterChipStatusDot'); + expect(workloadsFilterSource).not.toContain('const statusDot = (className: string)'); + expect(workloadsFilterSource).not.toContain('h-2 w-2 rounded-full ' + '${className}'); + }); + it('renders the search input', () => { render(() => ); expect(screen.getByTestId('search-input')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx b/frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx new file mode 100644 index 0000000000..2f7e07f1c1 --- /dev/null +++ b/frontend-modern/src/components/shared/FilterBar/filterOptionPresentation.tsx @@ -0,0 +1,5 @@ +import type { JSX } from 'solid-js'; + +export const filterChipStatusDot = (className: string): JSX.Element => ( +