From 4e2b62e89bcdf142e9f7021d928aa3b7a86d128c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 1 May 2026 10:51:22 +0100 Subject: [PATCH] Retire legacy Storage and Workloads filter helpers after FilterBar migration Deletes files that no runtime path imports after the migration to chip-based FilterBar: Storage's three-layer filter indirection: - StorageFilter.tsx (legacy filter shell) - StorageControls.tsx (subtab + filter pass-through wrapper) - useStorageFilterToolbarModel.ts (legacy active-filter / reset hook) - useStoragePageControlsModel.ts (sortDisabled + groupBy gating) - useStorageControlsModel.ts (subtab + node-filter wiring) - their tests Workloads filter state hook: - useWorkloadsFilterState.ts (replaced by inline FilterBar wiring; countActiveWorkloadsFilters / hasActiveWorkloadsFilters stay in workloadsFilterModel.ts) - its test The StorageStatusFilter and StorageGroupByFilter type aliases that StorageFilter.tsx exported fold into the existing canonical types in storagePageState.ts (StorageStatusFilterValue) and storageModelCore.ts (StorageGroupKey), keeping useStorageFilterState.ts alive without the deleted shell. PageControls.tsx and its companion FilterToolbar primitives stay in the tree because the alert-history filter card and the Kubernetes deployments drawer still consume them. The canonical claim shifts: FilterBar is the chip-based shell for catalog-driven page filters (Infrastructure, Workloads, Storage, Recovery Protected items, Recovery events). PageControls remains for non-migrated surfaces. Subsystem contracts and registry updated: - frontend-primitives.md: adds FilterBar files to Canonical Files; describes FilterBar as the canonical page-level filter shell for catalog-driven resource lists; PageControls described as the legacy fallback for non-migrated surfaces. - performance-and-scalability.md: drops useWorkloadsFilterState references; describes Workloads' FilterBar / viewOptionsTrailing composition. - storage-recovery.md: replaces "PageControls toolbar rail" prose with FilterBar / viewOptionsTrailing for both Storage and Recovery events; notes the legacy three-layer indirection retired. - registry.json: drops deleted file paths from owned_files, verification.exact_files, and verification.path_policies entries. - canonical_completion_guard_test.py + subsystem_lookup_test.py: drop deleted file paths from hard-coded fixtures so the governance helper tests track the registry. - SharedPrimitives.guardrails.test.ts: adds regression assertions that StoragePageControls no longer imports the deleted shells. --- .../subsystems/frontend-primitives.md | 22 +- .../subsystems/performance-and-scalability.md | 32 +- .../v6/internal/subsystems/registry.json | 5 - .../internal/subsystems/storage-recovery.md | 49 +-- .../components/Storage/StorageControls.tsx | 119 ------- .../src/components/Storage/StorageFilter.tsx | 310 ------------------ .../Storage/StoragePageControls.tsx | 14 +- .../__tests__/StorageControls.test.tsx | 164 --------- .../__tests__/useStorageControlsModel.test.ts | 30 -- .../useStorageFilterToolbarModel.test.ts | 99 ------ .../useStoragePageControlsModel.test.ts | 34 -- .../Storage/useStorageControlsModel.ts | 24 -- .../Storage/useStorageFilterState.ts | 8 +- .../Storage/useStorageFilterToolbarModel.ts | 122 ------- .../Storage/useStoragePageControlsModel.ts | 34 -- ...loadsSurface.performance.contract.test.tsx | 7 +- .../__tests__/useWorkloadsFilterState.test.ts | 87 ----- .../Workloads/useWorkloadsFilterState.ts | 82 ----- .../shared/PageControls.guardrails.test.ts | 19 +- .../SharedPrimitives.guardrails.test.ts | 13 + .../canonical_completion_guard_test.py | 2 - .../release_control/subsystem_lookup_test.py | 2 - 22 files changed, 94 insertions(+), 1184 deletions(-) delete mode 100644 frontend-modern/src/components/Storage/StorageControls.tsx delete mode 100644 frontend-modern/src/components/Storage/StorageFilter.tsx delete mode 100644 frontend-modern/src/components/Storage/__tests__/StorageControls.test.tsx delete mode 100644 frontend-modern/src/components/Storage/__tests__/useStorageControlsModel.test.ts delete mode 100644 frontend-modern/src/components/Storage/__tests__/useStorageFilterToolbarModel.test.ts delete mode 100644 frontend-modern/src/components/Storage/__tests__/useStoragePageControlsModel.test.ts delete mode 100644 frontend-modern/src/components/Storage/useStorageControlsModel.ts delete mode 100644 frontend-modern/src/components/Storage/useStorageFilterToolbarModel.ts delete mode 100644 frontend-modern/src/components/Storage/useStoragePageControlsModel.ts delete mode 100644 frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts delete mode 100644 frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index a870498d9..a93b218cc 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -78,7 +78,11 @@ work extends shared components instead of creating new local variants. 52. `frontend-modern/src/utils/updatesPresentation.ts` 53. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts` 54. `tests/integration/tests/15-settings-shell-consistency.spec.ts` -55. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts` +55. `frontend-modern/src/components/shared/FilterBar/FilterBar.tsx` +56. `frontend-modern/src/components/shared/FilterBar/FilterChip.tsx` +57. `frontend-modern/src/components/shared/FilterBar/AddFilterMenu.tsx` +58. `frontend-modern/src/components/shared/FilterBar/filterCatalog.ts` +59. `frontend-modern/src/components/shared/FilterBar/index.ts` 56. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts` 57. `frontend-modern/src/features/` 58. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx` @@ -1637,6 +1641,22 @@ search-row, filter-row, and inline-leading-slot layout surface. Monitoring pages that need workspace tabs or count chips next to search should route that through the shared `searchLeading` slot instead of recreating a second local header strip above the control bar. + +Pages that filter a list-of-resources surface (Infrastructure, Workloads, +Storage, Recovery Protected items, Recovery events) compose the chip-based +`frontend-modern/src/components/shared/FilterBar/FilterBar.tsx` shell instead +of `PageControls`. Each page declares a `FilterDef[]` catalog (label, options, +value, defaultValue, group); `FilterBar` renders chips for active filters and +exposes the rest behind a "+ Filter" menu, with type-ahead at both the menu +and chip popovers (`AddFilterMenu` and `FilterChip`). View options +(grouping segmented control, charts toggle, columns picker, sort key) sit in +the `viewOptionsTrailing` slot and are not chips. Subtabs that switch the +rendered dataset (Recovery's Protected items vs Recovery events, Storage's +Pools vs Physical Disks) sit above the bar; they are navigation, not filters. +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. That same shared filter-toolbar boundary also owns controlled select continuity when filter options materialize asynchronously. `LabeledFilterSelect` must keep the caller-owned `value` visibly selected after option children arrive so 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 0740471be..56eae83e9 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -40,7 +40,6 @@ regression protection. 18. `frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts` 19. `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx` 20. `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` -21. `frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts` 22. `frontend-modern/src/components/Workloads/ThresholdSlider.tsx` 23. `frontend-modern/src/components/Workloads/thresholdSliderModel.ts` 24. `frontend-modern/src/components/Workloads/useThresholdSliderState.ts` @@ -97,7 +96,6 @@ regression protection. 75. `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` 76. `frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx` 77. `frontend-modern/src/components/Workloads/__tests__/WorkloadsFilter.test.tsx` -78. `frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts` 79. `frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts` 80. `frontend-modern/src/components/Workloads/MetricBar.test.tsx` 81. `frontend-modern/src/components/Workloads/__tests__/useMetricBarState.test.tsx` @@ -198,14 +196,7 @@ regression protection. duplicate hot-path type matching. 16. Extend grouped workload derivation, summary fallbacks, and grouped/windowed table presentation through `frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts`, extend viewport-driven grouped table synchronization through `frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts`, and extend node parent mapping through `frontend-modern/src/components/Workloads/workloadTopology.ts`, rather than rebuilding grouped selectors, summary snapshot math, scroll listeners, or topology lookups inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts` 17. Extend workload control defaults, persistent view preferences, keyboard reset behavior, column-visibility ownership, and tag-search flow through `frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts` and `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` rather than rebuilding sort/search/grouping state, reset drift, or column-toggle plumbing inside `frontend-modern/src/components/Workloads/useWorkloadsState.ts` -18. Extend workload filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Workloads/workloadsFilterModel.ts` and `frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx` - Workloads filter presentation may consume frontend-primitives responsive - toggle controls, the shared `PageControls` structured control deck, and the - shared filter-section chrome for its semantic primary and secondary filter - groups, but the workload-owned state model must remain the source of truth - for active filter counting, reset behavior, and mobile collapse so changing - between wide toggle and narrow select presentation does not add a second - filter state path or layout-measurement path on the workload hot path. +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 chip-based shared `FilterBar` (`frontend-modern/src/components/shared/FilterBar/FilterBar.tsx`) with a per-page `FilterDef[]` catalog rather than the legacy `PageControls` structured control deck. The xl segmented↔select swap retired with the migration; type-ahead in the "+ Filter" menu and chip popovers covers the power-user speed that the segmented controls used to give. View options (grouped/list, charts, columns) sit in the shared `viewOptionsTrailing` slot. 19. Extend threshold-slider value-position math, title/label derivation, and drag scroll-lock runtime through `frontend-modern/src/components/Workloads/thresholdSliderModel.ts` and `frontend-modern/src/components/Workloads/useThresholdSliderState.ts` rather than rebuilding slider-local state and pointer lifecycle inside `frontend-modern/src/components/Workloads/ThresholdSlider.tsx` 20. Extend stacked disk-bar capacity math, segment/tooltip derivation, and resize-observer runtime through `frontend-modern/src/components/Workloads/stackedDiskBarModel.ts` and `frontend-modern/src/components/Workloads/useStackedDiskBarState.ts` rather than rebuilding disk-bar-local state, mode branching, and tooltip shaping inside `frontend-modern/src/components/Workloads/StackedDiskBar.tsx` 21. Extend stacked memory-bar capacity math, balloon/swap derivation, and resize-observer runtime through `frontend-modern/src/components/Workloads/stackedMemoryBarModel.ts` and `frontend-modern/src/components/Workloads/useStackedMemoryBarState.ts` rather than rebuilding memory-bar-local state, tooltip shaping, and label-fit logic inside `frontend-modern/src/components/Workloads/StackedMemoryBar.tsx` @@ -834,21 +825,24 @@ presentation derivations and fallback tooltip/runtime wiring live in usage math, threshold-color routing, and fallback handling must extend through those owners instead of accreting back into the shell. The Workloads filter now follows that same ownership rule: the shell -stays in `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`, while -toolbar defaults, active-filter counting, and reset semantics live in -`frontend-modern/src/components/Workloads/workloadsFilterModel.ts` and -`frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts`. +stays in `frontend-modern/src/components/Workloads/WorkloadsFilter.tsx`, which +composes the shared `FilterBar` chip primitive, while toolbar defaults, +active-filter counting, and reset semantics live in +`frontend-modern/src/components/Workloads/workloadsFilterModel.ts`. The legacy +`useWorkloadsFilterState.ts` hook retired with the FilterBar migration; its +`countActiveWorkloadsFilters` and `hasActiveWorkloadsFilters` helpers stay on +`workloadsFilterModel.ts`. Workloads table-mode controls must also keep their accessible group name aligned with the shared table presentation contract by using `Group by` for the grouped/list selector instead of reintroducing local `Group By` casing or platform-specific cluster wording. Dense workload toolbar variants must keep that same row wrap-capable so optional runtime, chart, column, and reset controls remain reachable on desktop instead of forcing a single no-wrap row -that clips trailing actions. The workload shell must route table display -actions such as grouped/list mode and chart visibility through -`PageControls.toolbarTrailing`, leaving route/filter selects as primary toolbar -children so the display-action cluster wraps together with Columns/Reset across -narrow desktop widths. Workload chart visibility is a display preference, not +that clips trailing actions. The workload shell now routes table display +actions such as grouped/list mode and chart visibility through the shared +`FilterBar.viewOptionsTrailing` slot, leaving filter chips as primary toolbar +children so the display-action cluster wraps together with Columns and the +"+ Filter" menu across narrow desktop widths. Workload chart visibility is a display preference, not an in-summary collapse affordance: the toolbar action must expose explicit `Show charts` / `Hide charts` pressed state, and hiding charts must remove the summary section rather than leaving an empty collapsed summary band on screen. diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index d43845835..068049798 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -4208,7 +4208,6 @@ "frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts", "frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts", "frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts", - "frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts", "frontend-modern/src/components/Workloads/useWorkloadsState.ts", "frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts", "frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts", @@ -4252,7 +4251,6 @@ "frontend-modern/src/components/Workloads/__tests__/useStackedMemoryBarState.test.tsx", "frontend-modern/src/components/Workloads/__tests__/useThresholdSliderState.test.ts", "frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts", - "frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts", "frontend-modern/src/components/Workloads/__tests__/useWorkloadViewportSync.test.tsx", "frontend-modern/src/components/Workloads/__tests__/workloadFilterConfigModel.test.ts", "frontend-modern/src/components/Workloads/__tests__/workloadRouteModel.test.ts", @@ -4357,7 +4355,6 @@ "frontend-modern/src/components/Workloads/useWorkloadsControlsState.ts", "frontend-modern/src/components/Workloads/useWorkloadsDerivedState.ts", "frontend-modern/src/components/Workloads/useWorkloadSelectionState.ts", - "frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts", "frontend-modern/src/components/Workloads/useWorkloadsState.ts", "frontend-modern/src/components/Workloads/useWorkloadUrlSync.ts", "frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts", @@ -4398,7 +4395,6 @@ "frontend-modern/src/components/Workloads/__tests__/useStackedMemoryBarState.test.tsx", "frontend-modern/src/components/Workloads/__tests__/useThresholdSliderState.test.ts", "frontend-modern/src/components/Workloads/__tests__/useWorkloadSelectionState.test.ts", - "frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts", "frontend-modern/src/components/Workloads/__tests__/useWorkloadViewportSync.test.tsx", "frontend-modern/src/components/Workloads/__tests__/workloadFilterConfigModel.test.ts", "frontend-modern/src/components/Workloads/__tests__/workloadRouteModel.test.ts", @@ -4875,7 +4871,6 @@ "frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts", "frontend-modern/src/components/Storage/__tests__/DiskList.test.tsx", "frontend-modern/src/components/Storage/__tests__/Storage.test.tsx", - "frontend-modern/src/components/Storage/__tests__/StorageControls.test.tsx", "frontend-modern/src/components/Storage/__tests__/StorageGroupRow.test.tsx", "frontend-modern/src/components/Storage/__tests__/StoragePoolDetail.test.tsx", "frontend-modern/src/components/Storage/__tests__/useStoragePageSummary.test.ts", diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 941520eb0..7dd423fc2 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -79,16 +79,20 @@ state. `overflow-x-auto` div around the shared table. Storage pools and physical disks inherit the surrounding `StorageContentCard` frame and must not add a nested `Card` or second scroll wrapper. - Recovery event filters must also keep page-level search, advanced filter, - column visibility, and reset controls on the shared `PageControls` toolbar - rail. Recovery must not reintroduce local no-wrap toolbar overrides or - right-aligned utility wrappers that prevent the frontend-primitives action - rail and structured control deck from owning responsive collapse behavior - and visible filter/action section boundaries. - Compact stable recovery-event filters, such as event outcome/status, may use - the frontend-primitives responsive toggle/select control, but Recovery must - keep route-backed event filter state and query semantics in its recovery - owner rather than introducing a page-local presentation state path. + Recovery event filters now compose the shared chip-based `FilterBar` + (`frontend-modern/src/components/shared/FilterBar/FilterBar.tsx`) with a + `FilterDef[]` catalog. The legacy "advanced filter popover" retired: scope, + method, verification, cluster, node, and namespace fold into the same chip + catalog as Item Type, Platform, and Status. Page-level search, column + visibility, and the rollup item filter live in the shared search row; the + chip row appears below it only when filters are active. Recovery must not + reintroduce a parallel `PageControls` toolbar or local no-wrap overrides + that bypass the FilterBar shell. + Compact stable recovery-event filters, such as event outcome/status, ride + the same chip popover affordance the rest of the catalog uses, with + type-ahead in the "+ Filter" menu and chip popovers, but Recovery must keep + route-backed event filter state and query semantics in its recovery owner + rather than introducing a page-local presentation state path. Recovery inventory protection posture and recovery-event outcome filtering must stay separate in that owner: protected inventory uses the route-backed `state` query for health, stale, failed, warning, running, unknown, and @@ -103,17 +107,20 @@ state. protected-item, and recovery-outcome readiness claims belong on the Storage and Recovery surfaces or their shared summary components, not in a restored dashboard panel cluster or Assistant brief. - Storage summary chart visibility is a page-level display preference owned - by the Storage page model and exposed through the shared `PageControls` - trailing action rail. Storage must keep the charts toggle, column picker, - and reset affordance on that shared rail so the Workloads and Storage - filter sections collapse through the same primitive contract. The charts - toggle must read as an explicit `Show charts` / `Hide charts` pressed - display action, and the off state must remove the summary section fully - instead of leaving a collapsed summary shell in the interface. Storage - filters inherit the shared `PageControls` structured deck and must not - duplicate page-local control-deck, action-rail, border, or background class - strings. + Storage filters now compose the shared chip-based `FilterBar` shell + through `frontend-modern/src/components/Storage/StoragePageControls.tsx`. + The legacy three-layer indirection + (`StoragePageControls` → `StorageControls` → `StorageFilter`) and the + bare unlabelled Node ` model.handleNodeFilterChange(event.currentTarget.value)} - class={STORAGE_CONTROLS_NODE_SELECT_CLASS} - aria-label="Node" - > - - {(option) => } - - -
- - ); - - return ( - <> - - - props.setSortKey(value as StorageSortKey)} - sortDirection={props.sortDirection} - setSortDirection={props.setSortDirection} - sortOptions={DEFAULT_STORAGE_SORT_OPTIONS} - sortDisabled={props.sortDisabled} - statusFilter={props.statusFilter} - setStatusFilter={props.setStatusFilter} - sourceFilter={props.sourceFilter} - setSourceFilter={props.setSourceFilter} - sourceOptions={props.sourceOptions} - diskRoleFilter={props.diskRoleFilter} - setDiskRoleFilter={props.setDiskRoleFilter} - diskRoleOptions={props.diskRoleOptions} - diskGroupFilter={props.diskGroupFilter} - setDiskGroupFilter={props.setDiskGroupFilter} - diskGroupOptions={props.diskGroupOptions} - selectedNodeId={props.selectedNodeId} - setSelectedNodeId={props.setSelectedNodeId} - leadingFilters={leadingFilters()} - chartsCollapsed={props.chartsCollapsed} - onChartsToggle={props.onChartsToggle} - mobileTrailing={props.mobileTrailing} - utilityActions={props.utilityActions} - /> - - ); -}; - -export default StorageControls; diff --git a/frontend-modern/src/components/Storage/StorageFilter.tsx b/frontend-modern/src/components/Storage/StorageFilter.tsx deleted file mode 100644 index 53b407152..000000000 --- a/frontend-modern/src/components/Storage/StorageFilter.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { Component, For, Show, type JSX } from 'solid-js'; -import { Card } from '@/components/shared/Card'; -import { - ChartVisibilityToggleButton, - FilterDivider, - FilterSegmentedControl, - LabeledFilterSelect, -} from '@/components/shared/FilterToolbar'; -import { PageControls } from '@/components/shared/PageControls'; -import { SearchInput } from '@/components/shared/SearchInput'; -import type { ColumnDef } from '@/hooks/useColumnVisibility'; -import { useBreakpoint } from '@/hooks/useBreakpoint'; -import { STORAGE_KEYS } from '@/utils/localStorage'; -import type { StorageSourceOption } from '@/utils/storageSources'; -import type { PhysicalDiskFilterOption } from '@/features/storageBackups/diskPresentation'; -import { - STORAGE_FILTER_COMPACT_SELECT_CLASS, - STORAGE_FILTER_RESET_ACTION_CLASS, - STORAGE_FILTER_SEGMENTED_WRAP_CLASS, - STORAGE_FILTER_SORT_DIRECTION_BUTTON_CLASS, - STORAGE_FILTER_SORT_ICON_CLASS, - STORAGE_FILTER_SORT_WRAP_CLASS, - STORAGE_FILTER_SORT_SELECT_CLASS, -} from '@/features/storageBackups/storageFilterPresentation'; -import { - DEFAULT_STORAGE_SORT_OPTIONS, - STORAGE_GROUP_BY_OPTIONS, - STORAGE_STATUS_FILTER_OPTIONS, -} from './storagePageState'; -import { useStorageFilterToolbarModel } from './useStorageFilterToolbarModel'; - -export type StorageStatusFilter = - | 'all' - | 'attention' - | 'available' - | 'warning' - | 'critical' - | 'offline' - | 'unknown'; -export type StorageGroupByFilter = 'node' | 'type' | 'status' | 'none'; - -interface StorageFilterProps { - search: () => string; - setSearch: (value: string) => void; - searchTrailing?: JSX.Element; - groupBy?: () => StorageGroupByFilter; - setGroupBy?: (value: StorageGroupByFilter) => void; - sortKey: () => string; - setSortKey: (value: string) => void; - sortDirection: () => 'asc' | 'desc'; - setSortDirection: (value: 'asc' | 'desc') => void; - sortOptions?: { value: string; label: string }[]; - sortDisabled?: boolean; - statusFilter?: () => StorageStatusFilter; - setStatusFilter?: (value: StorageStatusFilter) => void; - sourceFilter?: () => string; - setSourceFilter?: (value: string) => void; - sourceOptions?: () => StorageSourceOption[]; - diskRoleFilter?: () => string; - setDiskRoleFilter?: (value: string) => void; - diskRoleOptions?: () => PhysicalDiskFilterOption[]; - diskGroupFilter?: () => string; - setDiskGroupFilter?: (value: string) => void; - diskGroupOptions?: () => PhysicalDiskFilterOption[]; - selectedNodeId?: () => string; - setSelectedNodeId?: (value: string) => void; - chartsCollapsed?: () => boolean; - onChartsToggle?: () => void; - // Slot for page-specific filters (e.g., view toggle, node selector). - leadingFilters?: JSX.Element; - // Column visibility (optional) - columnVisibility?: { - availableToggles: () => ColumnDef[]; - isHiddenByUser: (id: string) => boolean; - toggle: (id: string) => void; - resetToDefaults: () => void; - }; - mobileTrailing?: JSX.Element; - utilityActions?: JSX.Element; -} - -export const StorageFilter: Component = (props) => { - const { isMobile } = useBreakpoint(); - const { - filtersOpen, - setFiltersOpen, - activeFilterCount, - showReset, - sortOptions, - sourceOptions, - sortDirectionTitle, - sortDirectionIconClass, - toggleSortDirection, - resetFilters, - } = useStorageFilterToolbarModel({ - search: props.search, - setSearch: props.setSearch, - groupBy: props.groupBy, - setGroupBy: props.setGroupBy, - sortKey: props.sortKey, - setSortKey: props.setSortKey, - sortDirection: props.sortDirection, - setSortDirection: props.setSortDirection, - statusFilter: props.statusFilter, - setStatusFilter: props.setStatusFilter, - sourceFilter: props.sourceFilter, - setSourceFilter: props.setSourceFilter, - diskRoleFilter: props.diskRoleFilter, - setDiskRoleFilter: props.setDiskRoleFilter, - diskGroupFilter: props.diskGroupFilter, - setDiskGroupFilter: props.setDiskGroupFilter, - selectedNodeId: props.selectedNodeId, - setSelectedNodeId: props.setSelectedNodeId, - sortOptions: props.sortOptions ?? DEFAULT_STORAGE_SORT_OPTIONS, - sourceOptions: props.sourceOptions, - }); - const chartsToolbarAction = () => - props.onChartsToggle ? ( - props.onChartsToggle?.()} - /> - ) : undefined; - - return ( - - - } - searchTrailing={props.searchTrailing} - mobileFilters={{ - enabled: isMobile(), - onToggle: () => setFiltersOpen((o) => !o), - count: activeFilterCount(), - }} - mobileTrailing={props.mobileTrailing} - columnVisibility={props.columnVisibility} - resetAction={{ - show: showReset(), - onClick: resetFilters, - title: 'Reset all filters', - class: STORAGE_FILTER_RESET_ACTION_CLASS, - icon: ( - - - - - - - ), - }} - showFilters={!isMobile() || filtersOpen()} - toolbarClass="gap-x-1.5 gap-y-2 sm:gap-x-2" - toolbarTrailing={chartsToolbarAction()} - utilityActions={props.utilityActions} - > - {props.leadingFilters} - - -
- props.setGroupBy!(value as StorageGroupByFilter)} - aria-label="Group by" - options={STORAGE_GROUP_BY_OPTIONS} - /> -
- -
- - - props.setSourceFilter!(e.currentTarget.value)} - selectClass={STORAGE_FILTER_COMPACT_SELECT_CLASS} - > - - {(option: StorageSourceOption) => } - - - - - - 1 - } - > - props.setDiskRoleFilter!(e.currentTarget.value)} - selectClass={STORAGE_FILTER_COMPACT_SELECT_CLASS} - > - - {(option: PhysicalDiskFilterOption) => ( - - )} - - - - - - 1 - } - > - props.setDiskGroupFilter!(e.currentTarget.value)} - selectClass={STORAGE_FILTER_COMPACT_SELECT_CLASS} - > - - {(option: PhysicalDiskFilterOption) => ( - - )} - - - - - - props.setStatusFilter?.(e.currentTarget.value as StorageStatusFilter)} - selectClass={STORAGE_FILTER_COMPACT_SELECT_CLASS} - > - {STORAGE_STATUS_FILTER_OPTIONS.map((option) => ( - - ))} - - - - -
- - -
-
-
- ); -}; diff --git a/frontend-modern/src/components/Storage/StoragePageControls.tsx b/frontend-modern/src/components/Storage/StoragePageControls.tsx index d7469cee8..30235e7c2 100644 --- a/frontend-modern/src/components/Storage/StoragePageControls.tsx +++ b/frontend-modern/src/components/Storage/StoragePageControls.tsx @@ -30,10 +30,10 @@ import { normalizeStorageSortKey, STORAGE_GROUP_BY_OPTIONS, STORAGE_STATUS_FILTER_OPTIONS, + type StorageStatusFilterValue, type StorageView, } from './storagePageState'; import type { StorageGroupKey, StorageSortKey } from './useStorageModel'; -import type { StorageGroupByFilter, StorageStatusFilter } from './StorageFilter'; import type { StorageSourceOption } from '@/utils/storageSources'; type StoragePageControlsProps = { @@ -49,8 +49,8 @@ type StoragePageControlsProps = { setSortKey: (value: StorageSortKey) => void; sortDirection: () => 'asc' | 'desc'; setSortDirection: (value: 'asc' | 'desc') => void; - statusFilter: () => StorageStatusFilter; - setStatusFilter: (value: StorageStatusFilter) => void; + statusFilter: () => StorageStatusFilterValue; + setStatusFilter: (value: StorageStatusFilterValue) => void; sourceFilter: () => string; setSourceFilter: (value: string) => void; sourceOptions: () => StorageSourceOption[]; @@ -63,7 +63,7 @@ type StoragePageControlsProps = { nodeFilterOptions: Array<{ value: string; label: string }>; selectedNodeId: () => string; setSelectedNodeId: (value: string) => void; - storageFilterGroupBy: () => StorageGroupByFilter; + storageFilterGroupBy: () => StorageGroupKey; chartsCollapsed?: () => boolean; onChartsToggle?: () => void; }; @@ -96,7 +96,7 @@ export const StoragePageControls: Component = (props) props.setSortKey(DEFAULT_STORAGE_SORT_KEY); props.setSortDirection(DEFAULT_STORAGE_SORT_DIRECTION); props.setGroupBy(DEFAULT_STORAGE_GROUP_KEY); - props.setStatusFilter(DEFAULT_STORAGE_STATUS_FILTER as StorageStatusFilter); + props.setStatusFilter(DEFAULT_STORAGE_STATUS_FILTER as StorageStatusFilterValue); props.setSourceFilter(DEFAULT_STORAGE_SOURCE_FILTER); props.setDiskRoleFilter?.(DEFAULT_PHYSICAL_DISK_FACET_FILTER); props.setDiskGroupFilter?.(DEFAULT_PHYSICAL_DISK_FACET_FILTER); @@ -155,8 +155,8 @@ export const StoragePageControls: Component = (props) group: 'status', value: () => props.statusFilter(), setValue: (value: string) => - props.setStatusFilter(value as StorageStatusFilter), - defaultValue: DEFAULT_STORAGE_STATUS_FILTER as StorageStatusFilter, + props.setStatusFilter(value as StorageStatusFilterValue), + defaultValue: DEFAULT_STORAGE_STATUS_FILTER as StorageStatusFilterValue, options: () => STORAGE_STATUS_FILTER_OPTIONS.map((option) => ({ value: option.value as string, diff --git a/frontend-modern/src/components/Storage/__tests__/StorageControls.test.tsx b/frontend-modern/src/components/Storage/__tests__/StorageControls.test.tsx deleted file mode 100644 index 9bed1e914..000000000 --- a/frontend-modern/src/components/Storage/__tests__/StorageControls.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; -import { createSignal } from 'solid-js'; -import { describe, expect, it, vi } from 'vitest'; -import { StorageControls } from '@/components/Storage/StorageControls'; -import type { StorageSourceOption } from '@/utils/storageSources'; - -describe('StorageControls', () => { - it('renders the shared storage controls and node filter', () => { - const [view, setView] = createSignal<'pools' | 'disks'>('pools'); - const [search, setSearch] = createSignal(''); - const [groupBy, setGroupBy] = createSignal<'none' | 'node'>('node'); - const [sortKey, setSortKey] = createSignal<'priority' | 'name' | 'usage' | 'type'>('name'); - const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); - const [statusFilter, setStatusFilter] = createSignal<'all' | 'warning' | 'critical'>('all'); - const [sourceFilter, setSourceFilter] = createSignal('all'); - const [sourceOptions] = createSignal([ - { key: 'all', label: 'All sources', tone: 'slate' as const }, - { key: 'proxmox-pve', label: 'PVE', tone: 'orange' as const }, - ]); - const [selectedNodeId, setSelectedNodeId] = createSignal('all'); - - render(() => ( - - )); - - expect(screen.getByLabelText('Storage view')).toBeInTheDocument(); - expect(screen.getByLabelText('Node')).toBeInTheDocument(); - - fireEvent.change(screen.getByLabelText('Node'), { - target: { value: 'node-1' }, - }); - expect(selectedNodeId()).toBe('node-1'); - }); - - it('updates source filter options when storage data arrives after first render', async () => { - const [view, setView] = createSignal<'pools' | 'disks'>('pools'); - const [search, setSearch] = createSignal(''); - const [groupBy, setGroupBy] = createSignal<'none' | 'node'>('node'); - const [sortKey, setSortKey] = createSignal<'priority' | 'name' | 'usage' | 'type'>('name'); - const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); - const [statusFilter, setStatusFilter] = createSignal<'all' | 'warning' | 'critical'>('all'); - const [sourceFilter, setSourceFilter] = createSignal('truenas'); - const [sourceOptions, setSourceOptions] = createSignal([ - { key: 'all', label: 'All sources', tone: 'slate' as const }, - ]); - const [selectedNodeId, setSelectedNodeId] = createSignal('all'); - - render(() => ( - - )); - - setSourceOptions([ - { key: 'all', label: 'All sources', tone: 'slate' as const }, - { key: 'proxmox-pve', label: 'PVE', tone: 'orange' as const }, - { key: 'truenas', label: 'TrueNAS', tone: 'blue' as const }, - ]); - - expect( - Array.from(screen.getByLabelText('Source').querySelectorAll('option')).map((option) => ({ - value: option.value, - label: option.textContent, - })), - ).toEqual([ - { value: 'all', label: 'All sources' }, - { value: 'proxmox-pve', label: 'PVE' }, - { value: 'truenas', label: 'TrueNAS' }, - ]); - await waitFor(() => { - expect(screen.getByLabelText('Source')).toHaveValue('truenas'); - }); - }); - - it('routes the charts toggle through the shared toolbar action rail', () => { - const [view, setView] = createSignal<'pools' | 'disks'>('pools'); - const [search, setSearch] = createSignal(''); - const [groupBy, setGroupBy] = createSignal<'none' | 'node'>('node'); - const [sortKey, setSortKey] = createSignal<'priority' | 'name' | 'usage' | 'type'>('name'); - const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); - const [statusFilter, setStatusFilter] = createSignal<'all' | 'warning' | 'critical'>('all'); - const [sourceFilter, setSourceFilter] = createSignal('all'); - const [sourceOptions] = createSignal([ - { key: 'all', label: 'All sources', tone: 'slate' as const }, - ]); - const [selectedNodeId, setSelectedNodeId] = createSignal('all'); - const [chartsCollapsed] = createSignal(false); - const onChartsToggle = vi.fn(); - - render(() => ( - - )); - - const chartsButton = screen.getByRole('button', { name: 'Hide charts' }); - expect(chartsButton.closest('.page-controls-toolbar-actions')).not.toBeNull(); - expect(chartsButton).toHaveTextContent('Charts'); - expect(chartsButton).toHaveAttribute('aria-pressed', 'true'); - expect(chartsButton).toHaveAttribute('title', 'Hide charts'); - - fireEvent.click(chartsButton); - - expect(onChartsToggle).toHaveBeenCalledTimes(1); - }); -}); diff --git a/frontend-modern/src/components/Storage/__tests__/useStorageControlsModel.test.ts b/frontend-modern/src/components/Storage/__tests__/useStorageControlsModel.test.ts deleted file mode 100644 index 25729d219..000000000 --- a/frontend-modern/src/components/Storage/__tests__/useStorageControlsModel.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { renderHook } from '@solidjs/testing-library'; -import { createSignal } from 'solid-js'; -import { describe, expect, it, vi } from 'vitest'; -import { useStorageControlsModel } from '@/components/Storage/useStorageControlsModel'; - -describe('useStorageControlsModel', () => { - it('centralizes storage controls view and node handlers', () => { - const [selectedNodeId, setSelectedNodeId] = createSignal('all'); - const onViewChange = vi.fn(); - - const { result } = renderHook(() => - useStorageControlsModel({ - selectedNodeId, - setSelectedNodeId, - onViewChange, - }), - ); - - expect(result.viewTabs).toEqual([ - { value: 'pools', label: 'Pools' }, - { value: 'disks', label: 'Physical Disks' }, - ]); - - result.handleNodeFilterChange('node-1'); - expect(selectedNodeId()).toBe('node-1'); - - result.handleViewChange('disks'); - expect(onViewChange).toHaveBeenCalledWith('disks'); - }); -}); diff --git a/frontend-modern/src/components/Storage/__tests__/useStorageFilterToolbarModel.test.ts b/frontend-modern/src/components/Storage/__tests__/useStorageFilterToolbarModel.test.ts deleted file mode 100644 index 15e6cdbd7..000000000 --- a/frontend-modern/src/components/Storage/__tests__/useStorageFilterToolbarModel.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { renderHook } from '@solidjs/testing-library'; -import { createSignal } from 'solid-js'; -import { describe, expect, it } from 'vitest'; -import { useStorageFilterToolbarModel } from '@/components/Storage/useStorageFilterToolbarModel'; -import type { StorageSourceOption } from '@/utils/storageSources'; - -describe('useStorageFilterToolbarModel', () => { - it('centralizes storage filter toolbar state and reset behavior', () => { - const [search, setSearch] = createSignal('tank'); - const [groupBy, setGroupBy] = createSignal<'none' | 'node'>('node'); - const [sortKey, setSortKey] = createSignal('usage'); - const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc'); - const [statusFilter, setStatusFilter] = createSignal<'all' | 'warning'>('warning'); - const [sourceFilter, setSourceFilter] = createSignal('agent'); - const [diskRoleFilter, setDiskRoleFilter] = createSignal('nvme-disk'); - const [diskGroupFilter, setDiskGroupFilter] = createSignal('data'); - const [selectedNodeId, setSelectedNodeId] = createSignal('node-1'); - - const { result } = renderHook(() => - useStorageFilterToolbarModel({ - search, - setSearch, - groupBy, - setGroupBy, - sortKey, - setSortKey, - sortDirection, - setSortDirection, - statusFilter, - setStatusFilter, - sourceFilter, - setSourceFilter, - diskRoleFilter, - setDiskRoleFilter, - diskGroupFilter, - setDiskGroupFilter, - selectedNodeId, - setSelectedNodeId, - sortOptions: [{ value: 'usage', label: 'Usage' }], - }), - ); - - expect(result.activeFilterCount()).toBeGreaterThan(0); - expect(result.showReset()).toBe(true); - expect(result.sortOptions()).toEqual([{ value: 'usage', label: 'Usage' }]); - expect(result.sortDirectionTitle()).toBe('Sort descending'); - expect(result.sortDirectionIconClass()).toBe('rotate-180'); - - result.toggleSortDirection(); - expect(sortDirection()).toBe('desc'); - - result.resetFilters(); - - expect(search()).toBe(''); - expect(groupBy()).toBe('none'); - expect(sortKey()).toBe('priority'); - expect(sortDirection()).toBe('desc'); - expect(statusFilter()).toBe('all'); - expect(sourceFilter()).toBe('all'); - expect(diskRoleFilter()).toBe('all'); - expect(diskGroupFilter()).toBe('all'); - expect(selectedNodeId()).toBe('all'); - }); - - it('keeps storage source options reactive for restored route state', () => { - const [search, setSearch] = createSignal(''); - const [sortKey, setSortKey] = createSignal('priority'); - const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('desc'); - const [sourceOptions, setSourceOptions] = createSignal([ - { key: 'all', label: 'All sources', tone: 'slate' as const }, - ]); - - const { result } = renderHook(() => - useStorageFilterToolbarModel({ - search, - setSearch, - sortKey, - setSortKey, - sortDirection, - setSortDirection, - sourceOptions, - }), - ); - - expect(result.sourceOptions()).toEqual([{ key: 'all', label: 'All sources', tone: 'slate' }]); - - setSourceOptions([ - { key: 'all', label: 'All sources', tone: 'slate' }, - { key: 'proxmox-pve', label: 'PVE', tone: 'orange' }, - { key: 'truenas', label: 'TrueNAS', tone: 'blue' }, - ]); - - expect(result.sourceOptions()).toEqual([ - { key: 'all', label: 'All sources', tone: 'slate' }, - { key: 'proxmox-pve', label: 'PVE', tone: 'orange' }, - { key: 'truenas', label: 'TrueNAS', tone: 'blue' }, - ]); - }); -}); diff --git a/frontend-modern/src/components/Storage/__tests__/useStoragePageControlsModel.test.ts b/frontend-modern/src/components/Storage/__tests__/useStoragePageControlsModel.test.ts deleted file mode 100644 index 4667e5e72..000000000 --- a/frontend-modern/src/components/Storage/__tests__/useStoragePageControlsModel.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { renderHook } from '@solidjs/testing-library'; -import { createSignal } from 'solid-js'; -import { describe, expect, it, vi } from 'vitest'; -import { useStoragePageControlsModel } from '@/components/Storage/useStoragePageControlsModel'; - -describe('useStoragePageControlsModel', () => { - it('derives canonical controls wiring from page state', () => { - const [kioskMode] = createSignal(false); - const [view] = createSignal<'pools' | 'disks'>('pools'); - const setGroupBy = vi.fn(); - const setSortKey = vi.fn(); - const [storageFilterGroupBy] = createSignal<'none' | 'node'>('node'); - - const { result } = renderHook(() => - useStoragePageControlsModel({ - kioskMode, - view, - setGroupBy, - setSortKey, - storageFilterGroupBy, - }), - ); - - expect(result.showControls()).toBe(true); - expect(result.sortDisabled()).toBe(false); - expect(result.groupBy()?.()).toBe('node'); - - result.setGroupBy()?.('node'); - expect(setGroupBy).toHaveBeenCalledWith('node'); - - result.setNormalizedSortKey('type'); - expect(setSortKey).toHaveBeenCalledWith('type'); - }); -}); diff --git a/frontend-modern/src/components/Storage/useStorageControlsModel.ts b/frontend-modern/src/components/Storage/useStorageControlsModel.ts deleted file mode 100644 index 90ee33da1..000000000 --- a/frontend-modern/src/components/Storage/useStorageControlsModel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { STORAGE_VIEW_OPTIONS } from '@/features/storageBackups/storagePagePresentation'; -import type { StorageView } from './storagePageState'; - -type StorageControlsModelOptions = { - selectedNodeId: () => string; - setSelectedNodeId: (value: string) => void; - onViewChange: (value: StorageView) => void; -}; - -export const useStorageControlsModel = (options: StorageControlsModelOptions) => { - const handleNodeFilterChange = (value: string) => { - options.setSelectedNodeId(value); - }; - - const handleViewChange = (value: string) => { - options.onViewChange(value as StorageView); - }; - - return { - viewTabs: STORAGE_VIEW_OPTIONS as { value: string; label: string }[], - handleNodeFilterChange, - handleViewChange, - }; -}; diff --git a/frontend-modern/src/components/Storage/useStorageFilterState.ts b/frontend-modern/src/components/Storage/useStorageFilterState.ts index 773516d8e..40d750a03 100644 --- a/frontend-modern/src/components/Storage/useStorageFilterState.ts +++ b/frontend-modern/src/components/Storage/useStorageFilterState.ts @@ -18,10 +18,10 @@ import { getStorageFilterGroupBy, getStorageStatusFilterValue, toStorageHealthFilterValue, + type StorageStatusFilterValue, type StorageView, } from './storagePageState'; import type { StorageNodeOption, StorageGroupKey } from './useStorageModel'; -import type { StorageGroupByFilter, StorageStatusFilter } from './StorageFilter'; type UseStorageFilterStateOptions = { view: Accessor; @@ -80,15 +80,15 @@ export const useStorageFilterState = (options: UseStorageFilterStateOptions) => ], ); - const storageFilterGroupBy = (): StorageGroupByFilter => { + const storageFilterGroupBy = (): StorageGroupKey => { return getStorageFilterGroupBy(options.groupBy()); }; - const storageFilterStatus = (): StorageStatusFilter => { + const storageFilterStatus = (): StorageStatusFilterValue => { return getStorageStatusFilterValue(options.healthFilter()); }; - const setStorageFilterStatus = (value: StorageStatusFilter) => { + const setStorageFilterStatus = (value: StorageStatusFilterValue) => { options.setHealthFilter(toStorageHealthFilterValue(value)); }; diff --git a/frontend-modern/src/components/Storage/useStorageFilterToolbarModel.ts b/frontend-modern/src/components/Storage/useStorageFilterToolbarModel.ts deleted file mode 100644 index c69d870ad..000000000 --- a/frontend-modern/src/components/Storage/useStorageFilterToolbarModel.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createMemo, createSignal, type Accessor } from 'solid-js'; -import { DEFAULT_STORAGE_SOURCE_OPTIONS, type StorageSourceOption } from '@/utils/storageSources'; -import { - getNextStorageSortDirection, - getStorageSortDirectionIconClass, - getStorageSortDirectionTitle, -} from '@/features/storageBackups/storageFilterPresentation'; -import { - countActiveStorageFilters, - DEFAULT_STORAGE_GROUP_KEY, - DEFAULT_STORAGE_DISK_GROUP_FILTER, - DEFAULT_STORAGE_DISK_ROLE_FILTER, - DEFAULT_STORAGE_SORT_DIRECTION, - DEFAULT_STORAGE_SORT_KEY, - DEFAULT_STORAGE_SOURCE_FILTER, - DEFAULT_STORAGE_SELECTED_NODE_ID, - DEFAULT_STORAGE_STATUS_FILTER, - hasActiveStorageFilters, - type StorageOption, -} from './storagePageState'; -import type { StorageGroupByFilter, StorageStatusFilter } from './StorageFilter'; - -type UseStorageFilterToolbarModelOptions = { - search: Accessor; - setSearch: (value: string) => void; - groupBy?: Accessor; - setGroupBy?: (value: StorageGroupByFilter) => void; - sortKey: Accessor; - setSortKey: (value: string) => void; - sortDirection: Accessor<'asc' | 'desc'>; - setSortDirection: (value: 'asc' | 'desc') => void; - statusFilter?: Accessor; - setStatusFilter?: (value: StorageStatusFilter) => void; - sourceFilter?: Accessor; - setSourceFilter?: (value: string) => void; - diskRoleFilter?: Accessor; - setDiskRoleFilter?: (value: string) => void; - diskGroupFilter?: Accessor; - setDiskGroupFilter?: (value: string) => void; - selectedNodeId?: Accessor; - setSelectedNodeId?: (value: string) => void; - sortOptions?: StorageOption[]; - sourceOptions?: Accessor; -}; - -export const useStorageFilterToolbarModel = (options: UseStorageFilterToolbarModelOptions) => { - const [filtersOpen, setFiltersOpen] = createSignal(false); - - const activeFilterCount = createMemo(() => { - const nodeActive = - (options.selectedNodeId?.() || DEFAULT_STORAGE_SELECTED_NODE_ID) !== - DEFAULT_STORAGE_SELECTED_NODE_ID; - return ( - countActiveStorageFilters({ - search: options.search(), - sortKey: options.sortKey(), - sortDirection: options.sortDirection(), - groupBy: options.groupBy?.(), - statusFilter: options.statusFilter?.(), - sourceFilter: options.sourceFilter?.(), - diskRoleFilter: options.diskRoleFilter?.(), - diskGroupFilter: options.diskGroupFilter?.(), - }) + (nodeActive ? 1 : 0) - ); - }); - - const showReset = createMemo( - () => - hasActiveStorageFilters({ - search: options.search(), - sortKey: options.sortKey(), - sortDirection: options.sortDirection(), - groupBy: options.groupBy?.(), - statusFilter: options.statusFilter?.(), - sourceFilter: options.sourceFilter?.(), - diskRoleFilter: options.diskRoleFilter?.(), - diskGroupFilter: options.diskGroupFilter?.(), - }) || - (options.selectedNodeId?.() || DEFAULT_STORAGE_SELECTED_NODE_ID) !== - DEFAULT_STORAGE_SELECTED_NODE_ID, - ); - - const sortOptions = createMemo(() => options.sortOptions ?? []); - const sourceOptions = createMemo( - () => options.sourceOptions?.() ?? DEFAULT_STORAGE_SOURCE_OPTIONS, - ); - const sortDirectionTitle = createMemo(() => - getStorageSortDirectionTitle(options.sortDirection()), - ); - const sortDirectionIconClass = createMemo(() => - getStorageSortDirectionIconClass(options.sortDirection()), - ); - - const toggleSortDirection = () => { - options.setSortDirection(getNextStorageSortDirection(options.sortDirection())); - }; - - const resetFilters = () => { - options.setSearch(''); - options.setSortKey(DEFAULT_STORAGE_SORT_KEY); - options.setSortDirection(DEFAULT_STORAGE_SORT_DIRECTION); - if (options.setGroupBy) options.setGroupBy(DEFAULT_STORAGE_GROUP_KEY); - if (options.setStatusFilter) options.setStatusFilter(DEFAULT_STORAGE_STATUS_FILTER); - if (options.setSourceFilter) options.setSourceFilter(DEFAULT_STORAGE_SOURCE_FILTER); - if (options.setDiskRoleFilter) options.setDiskRoleFilter(DEFAULT_STORAGE_DISK_ROLE_FILTER); - if (options.setDiskGroupFilter) options.setDiskGroupFilter(DEFAULT_STORAGE_DISK_GROUP_FILTER); - if (options.setSelectedNodeId) options.setSelectedNodeId(DEFAULT_STORAGE_SELECTED_NODE_ID); - }; - - return { - filtersOpen, - setFiltersOpen, - activeFilterCount, - showReset, - sortOptions, - sourceOptions, - sortDirectionTitle, - sortDirectionIconClass, - toggleSortDirection, - resetFilters, - }; -}; diff --git a/frontend-modern/src/components/Storage/useStoragePageControlsModel.ts b/frontend-modern/src/components/Storage/useStoragePageControlsModel.ts deleted file mode 100644 index b363cd2e0..000000000 --- a/frontend-modern/src/components/Storage/useStoragePageControlsModel.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createMemo } from 'solid-js'; -import { normalizeStorageSortKey, type StorageView } from './storagePageState'; -import type { StorageGroupKey, StorageSortKey } from './useStorageModel'; -import type { StorageGroupByFilter } from './StorageFilter'; - -type UseStoragePageControlsModelOptions = { - kioskMode: () => boolean; - view: () => StorageView; - setGroupBy: (value: StorageGroupKey) => void; - setSortKey: (value: StorageSortKey) => void; - storageFilterGroupBy: () => StorageGroupByFilter; -}; - -export const useStoragePageControlsModel = (options: UseStoragePageControlsModelOptions) => { - const showControls = createMemo(() => !options.kioskMode()); - const sortDisabled = createMemo(() => options.view() !== 'pools'); - const groupBy = () => (options.view() === 'pools' ? options.storageFilterGroupBy : undefined); - const setGroupBy = () => - options.view() === 'pools' - ? ((value: StorageGroupByFilter) => options.setGroupBy(value as StorageGroupKey)) - : undefined; - - const setNormalizedSortKey = (value: StorageSortKey) => { - options.setSortKey(normalizeStorageSortKey(value)); - }; - - return { - showControls, - sortDisabled, - groupBy, - setGroupBy, - setNormalizedSortKey, - }; -}; diff --git a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx index 446d4e2b0..e0278712c 100644 --- a/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx @@ -25,7 +25,6 @@ import workloadsWorkloadViewportSyncSource from '../useWorkloadViewportSync.ts?r import workloadsWorkloadRouteStateSource from '../useWorkloadRouteState.ts?raw'; import workloadsWorkloadUrlSyncSource from '../useWorkloadUrlSync.ts?raw'; import workloadsStateSource from '../useWorkloadsState.ts?raw'; -import workloadsFilterStateSource from '../useWorkloadsFilterState.ts?raw'; import groupedTableWindowingSource from '../useGroupedTableWindowing.ts?raw'; import thresholdSliderSource from '../ThresholdSlider.tsx?raw'; import thresholdSliderModelSource from '../thresholdSliderModel.ts?raw'; @@ -811,10 +810,8 @@ describe('Workloads performance contract', () => { expect(workloadsFilterSource).not.toContain('const [filtersOpen, setFiltersOpen] ='); expect(workloadsFilterSource).not.toContain("props.setSortKey('name')"); expect(workloadsFilterSource).toContain('searchTrailing={props.searchTrailing}'); - expect(workloadsFilterStateSource).toContain('countActiveWorkloadsFilters'); - expect(workloadsFilterStateSource).not.toContain('props.containerRuntimeFilter?.onChange'); - expect(workloadsFilterStateSource).toContain('useBreakpoint'); - expect(workloadsFilterStateSource).toContain('DEFAULT_WORKLOADS_SORT_KEY'); + // useWorkloadsFilterState hook retired in the cleanup commit; the + // canonical defaults / activity helpers now live in workloadsFilterModel. expect(workloadsFilterModelSource).toContain('export const countActiveWorkloadsFilters'); expect(workloadsFilterModelSource).toContain('export const hasActiveWorkloadsFilters'); expect(workloadsFilterModelSource).toContain( diff --git a/frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts b/frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts deleted file mode 100644 index 19329b982..000000000 --- a/frontend-modern/src/components/Workloads/__tests__/useWorkloadsFilterState.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { renderHook } from '@solidjs/testing-library'; -import { createSignal } from 'solid-js'; -import { describe, expect, it, vi } from 'vitest'; -import { useWorkloadsFilterState } from '@/components/Workloads/useWorkloadsFilterState'; - -vi.mock('@/hooks/useBreakpoint', () => ({ - useBreakpoint: () => ({ - isMobile: () => false, - }), -})); - -describe('useWorkloadsFilterState', () => { - it('centralizes workloads filter state and reset behavior', () => { - const [search, setSearch] = createSignal('query'); - const [viewMode, setViewMode] = createSignal< - 'all' | 'vm' | 'container' | 'system-container' | 'app-container' | 'pod' - >('vm'); - const [statusMode, setStatusMode] = createSignal<'all' | 'running' | 'degraded' | 'stopped'>( - 'running', - ); - const [groupingMode, setGroupingMode] = createSignal<'grouped' | 'flat'>('flat'); - const [sortKey, setSortKey] = createSignal('cpu'); - const [sortDirection, setSortDirection] = createSignal('desc'); - const hostOnChange = vi.fn(); - const platformOnChange = vi.fn(); - const namespaceOnChange = vi.fn(); - const runtimeOnChange = vi.fn(); - - const { result } = renderHook(() => - useWorkloadsFilterState({ - search, - setSearch, - viewMode, - setViewMode, - statusMode, - setStatusMode, - groupingMode, - setGroupingMode, - setSortKey, - setSortDirection, - hostFilter: { - value: 'host-1', - options: [{ value: 'host-1', label: 'Host 1' }], - onChange: hostOnChange, - }, - platformFilter: { - value: 'truenas', - options: [{ value: 'truenas', label: 'TrueNAS' }], - onChange: platformOnChange, - }, - namespaceFilter: { - value: 'ns-1', - options: [{ value: 'ns-1', label: 'NS 1' }], - onChange: namespaceOnChange, - }, - containerRuntimeFilter: { - value: 'docker', - options: [{ value: 'docker', label: 'Docker' }], - onChange: runtimeOnChange, - }, - }), - ); - - expect(result.activeFilterCount()).toBe(6); - expect(result.showReset()).toBe(true); - expect(result.showToolbarFilters()).toBe(true); - expect(result.filtersOpen()).toBe(false); - expect(result.isMobile()).toBe(false); - - result.setFiltersOpen(true); - expect(result.filtersOpen()).toBe(true); - expect(result.showToolbarFilters()).toBe(true); - - result.resetFilters(); - - expect(search()).toBe(''); - expect(viewMode()).toBe('all'); - expect(statusMode()).toBe('all'); - expect(groupingMode()).toBe('grouped'); - expect(sortKey()).toBe('type'); - expect(sortDirection()).toBe('asc'); - expect(hostOnChange).toHaveBeenCalledWith(''); - expect(platformOnChange).toHaveBeenCalledWith(''); - expect(namespaceOnChange).toHaveBeenCalledWith(''); - expect(runtimeOnChange).not.toHaveBeenCalled(); - }); -}); diff --git a/frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts b/frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts deleted file mode 100644 index 235cccd91..000000000 --- a/frontend-modern/src/components/Workloads/useWorkloadsFilterState.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createMemo, createSignal } from 'solid-js'; - -import { useBreakpoint } from '@/hooks/useBreakpoint'; - -import { - countActiveWorkloadsFilters, - DEFAULT_WORKLOADS_GROUPING_MODE, - DEFAULT_WORKLOADS_SORT_DIRECTION, - DEFAULT_WORKLOADS_SORT_KEY, - DEFAULT_WORKLOADS_STATUS_MODE, - DEFAULT_WORKLOADS_VIEW_MODE, - hasActiveWorkloadsFilters, - type WorkloadsFilterProps, -} from './workloadsFilterModel'; - -export const useWorkloadsFilterState = (props: WorkloadsFilterProps) => { - const { isMobile } = useBreakpoint(); - const [filtersOpen, setFiltersOpen] = createSignal(false); - - const activeFilterCount = createMemo(() => - countActiveWorkloadsFilters({ - search: props.search(), - viewMode: props.viewMode(), - statusMode: props.statusMode(), - hostFilterValue: props.hostFilter?.value, - platformFilterValue: props.platformFilter?.value, - namespaceFilterValue: props.namespaceFilter?.value, - }), - ); - - const showReset = createMemo(() => - hasActiveWorkloadsFilters({ - search: props.search(), - viewMode: props.viewMode(), - statusMode: props.statusMode(), - groupingMode: props.groupingMode(), - hostFilterValue: props.hostFilter?.value, - platformFilterValue: props.platformFilter?.value, - namespaceFilterValue: props.namespaceFilter?.value, - }), - ); - - const pageControlsColumnVisibility = createMemo(() => - props.columnVisibility - ? { - availableToggles: () => props.columnVisibility!.availableColumns, - isHiddenByUser: props.columnVisibility!.isColumnHidden, - toggle: props.columnVisibility!.onColumnToggle, - resetToDefaults: props.columnVisibility!.onColumnReset ?? (() => undefined), - } - : undefined, - ); - - const toggleFilters = () => { - setFiltersOpen((open) => !open); - }; - - const resetFilters = () => { - props.setSearch(''); - props.setSortKey(DEFAULT_WORKLOADS_SORT_KEY); - props.setSortDirection(DEFAULT_WORKLOADS_SORT_DIRECTION); - props.setViewMode(DEFAULT_WORKLOADS_VIEW_MODE); - props.setStatusMode(DEFAULT_WORKLOADS_STATUS_MODE); - props.setGroupingMode(DEFAULT_WORKLOADS_GROUPING_MODE); - - props.hostFilter?.onChange(''); - props.platformFilter?.onChange(''); - props.namespaceFilter?.onChange(''); - }; - - return { - activeFilterCount, - filtersOpen, - isMobile, - pageControlsColumnVisibility, - resetFilters, - setFiltersOpen, - showReset, - showToolbarFilters: createMemo(() => !isMobile() || filtersOpen()), - toggleFilters, - }; -}; diff --git a/frontend-modern/src/components/shared/PageControls.guardrails.test.ts b/frontend-modern/src/components/shared/PageControls.guardrails.test.ts index 055466359..e0e9b2e79 100644 --- a/frontend-modern/src/components/shared/PageControls.guardrails.test.ts +++ b/frontend-modern/src/components/shared/PageControls.guardrails.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import pageControlsSource from '@/components/shared/PageControls.tsx?raw'; import filterToolbarSource from '@/components/shared/FilterToolbar.tsx?raw'; import workloadsFilterSource from '@/components/Workloads/WorkloadsFilter.tsx?raw'; -import storageFilterSource from '@/components/Storage/StorageFilter.tsx?raw'; import recoveryPageSource from '@/components/Recovery/Recovery.tsx?raw'; import recoveryHistorySectionSource from '@/components/Recovery/RecoveryHistorySection.tsx?raw'; import recoveryProtectedInventorySectionSource from '@/components/Recovery/RecoveryProtectedInventorySection.tsx?raw'; @@ -47,9 +46,8 @@ describe('page controls guardrails', () => { // FilterHeader check. expect(workloadsFilterSource).not.toContain(' { }); it('keeps display controls and utility actions on the shared toolbar rail', () => { - // WorkloadsFilter migrated to FilterBar's viewOptionsTrailing slot; the - // toolbarTrailing PageControls prop is no longer used. ChartVisibilityToggleButton - // and the no-aria-label-Charts regression guard still apply. - expect(storageFilterSource).toContain('toolbarTrailing={'); + // Workloads / Infrastructure / Storage migrated to FilterBar's + // viewOptionsTrailing slot; the toolbarTrailing PageControls prop is no + // longer used. ChartVisibilityToggleButton and the + // no-aria-label-Charts regression guard still apply. expect(workloadsFilterSource).toContain('ChartVisibilityToggleButton'); - expect(storageFilterSource).toContain('ChartVisibilityToggleButton'); expect(infrastructurePageSurfaceSource).toContain('ChartVisibilityToggleButton'); expect(workloadsFilterSource).not.toContain('aria-label="Charts"'); - expect(storageFilterSource).not.toContain('aria-label="Charts"'); expect(infrastructurePageSurfaceSource).not.toContain('aria-label="Charts"'); expect(pageControlsSource).toContain('page-controls-filter-controls'); expect(pageControlsSource).toContain('page-controls-toolbar-actions ml-auto'); @@ -192,9 +188,6 @@ describe('page controls guardrails', () => { // / LabeledFilterSelect no longer rendered. expect(recoveryHistorySectionSource).not.toContain('LabeledFilterToggleGroup'); expect(recoveryHistorySectionSource).not.toContain('LabeledFilterSelect'); - expect(storageFilterSource).toContain( - ' { expect(storagePageControlsSource).not.toContain('