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('