diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index adde3d6be6..ad6cc9b57d 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -7,7 +7,7 @@ import StoragePageSummary from '@/components/Storage/StoragePageSummary'; import { StorageViewSegmentedControl } from '@/components/Storage/StorageViewSegmentedControl'; import { PageHeader } from '@/components/shared/PageHeader'; import { StickySummarySection } from '@/components/shared/StickySummarySection'; -import { isStorageRecordCeph } from './storagePageState'; +import { DEFAULT_STORAGE_SELECTED_NODE_ID, isStorageRecordCeph } from './storagePageState'; import { useStoragePageModel } from './useStoragePageModel'; type StorageProps = { @@ -20,12 +20,14 @@ type StorageProps = { // `showFilterToolbar` so the canonical StoragePageControls row stays // visible alongside the table even when `tableOnly` hides the summary // section. The page owns source scope via `forcedSourceFilter`; the - // controls toolbar still exposes search, status, grouping, sort, and - // node filters to the operator. `suppressSourceFilter` drops the - // redundant Source chip from the controls toolbar since the platform - // page already locks source scope through `forcedSourceFilter`. + // controls toolbar still exposes search, status, grouping, and sort to + // the operator. `suppressSourceFilter` drops the redundant Source chip + // from the controls toolbar since the platform page already locks source + // scope through `forcedSourceFilter`; `suppressNodeFilter` lets platform + // pages use search for host/node scoping, matching their overview pages. showFilterToolbar?: boolean; suppressSourceFilter?: boolean; + suppressNodeFilter?: boolean; filterAriaLabel?: string; filterSearchPlaceholder?: string; filterSearchEmptyMessage?: string; @@ -123,6 +125,13 @@ const Storage: Component = (props) => { } }); + createEffect(() => { + if (!props.suppressNodeFilter) return; + if (selectedNodeId() !== DEFAULT_STORAGE_SELECTED_NODE_ID) { + setSelectedNodeId(DEFAULT_STORAGE_SELECTED_NODE_ID); + } + }); + return (
@@ -204,6 +213,7 @@ const Storage: Component = (props) => { setSourceFilter={setSourceFilter} sourceOptions={sourceFilterOptions} suppressSourceFilter={props.suppressSourceFilter || Boolean(props.forcedSourceFilter)} + suppressNodeFilter={props.suppressNodeFilter} diskRoleFilter={diskRoleFilter} setDiskRoleFilter={setDiskRoleFilter} diskRoleOptions={diskRoleOptions} diff --git a/frontend-modern/src/components/Storage/StoragePageControls.tsx b/frontend-modern/src/components/Storage/StoragePageControls.tsx index d393e7867e..f00ba0a3dd 100644 --- a/frontend-modern/src/components/Storage/StoragePageControls.tsx +++ b/frontend-modern/src/components/Storage/StoragePageControls.tsx @@ -76,6 +76,10 @@ type StoragePageControlsProps = { // and would render as a user-visible pinned filter. Setting this drops // the Source chip from the rendered filter row. suppressSourceFilter?: boolean; + // Platform-owned storage tabs use the search box for host/node scoping, + // matching the overview-page filter contract. Standalone /storage keeps + // the explicit Node filter so canonical node query links remain visible. + suppressNodeFilter?: boolean; }; const VIEW_TABS = STORAGE_VIEW_OPTIONS as { value: string; label: string }[]; @@ -116,8 +120,9 @@ export const StoragePageControls: Component = (props) diskRoleFilter: props.diskRoleFilter?.(), diskGroupFilter: props.diskGroupFilter?.(), }) || - (props.selectedNodeId() || DEFAULT_STORAGE_SELECTED_NODE_ID) !== - DEFAULT_STORAGE_SELECTED_NODE_ID, + (!props.suppressNodeFilter && + (props.selectedNodeId() || DEFAULT_STORAGE_SELECTED_NODE_ID) !== + DEFAULT_STORAGE_SELECTED_NODE_ID), ); const handleClearAll = () => { @@ -142,8 +147,10 @@ export const StoragePageControls: Component = (props) }; const buildFilters = (): FilterDef[] => { - const filters: FilterDef[] = [ - { + const filters: FilterDef[] = []; + + if (!props.suppressNodeFilter) { + filters.push({ id: 'storage-node', label: 'Node', group: 'scope', @@ -155,8 +162,8 @@ export const StoragePageControls: Component = (props) value: option.value, label: option.label, })), - }, - ]; + }); + } if (isPoolsView()) { filters.push({ diff --git a/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx b/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx index db6159dcb8..414a50472e 100644 --- a/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx +++ b/frontend-modern/src/components/Storage/__tests__/Storage.test.tsx @@ -365,6 +365,7 @@ describe('Storage platform-page embed contract', () => { it('exposes showFilterToolbar on StorageProps so platform pages keep StoragePageControls visible under tableOnly', async () => { const storageSource = (await import('../Storage.tsx?raw')).default; expect(storageSource).toContain('showFilterToolbar?: boolean;'); + expect(storageSource).toContain('suppressNodeFilter?: boolean;'); expect(storageSource).toContain('props.showFilterToolbar || !props.tableOnly'); }); @@ -377,6 +378,14 @@ describe('Storage platform-page embed contract', () => { expect(controlsSource).toContain('suppressSourceFilter?: boolean;'); expect(controlsSource).toContain('if (!props.suppressSourceFilter) {'); }); + + it('lets platform storage tabs suppress the explicit Node filter in favor of search', async () => { + const storageSource = (await import('../Storage.tsx?raw')).default; + expect(storageSource).toContain('suppressNodeFilter={props.suppressNodeFilter}'); + const controlsSource = (await import('../StoragePageControls.tsx?raw')).default; + expect(controlsSource).toContain('suppressNodeFilter?: boolean;'); + expect(controlsSource).toContain('if (!props.suppressNodeFilter) {'); + }); }); describe('Storage', () => { @@ -1890,6 +1899,34 @@ describe('Storage', () => { it('passes platform-owned filter copy into embedded storage controls', () => { mockLocationPath = '/vmware/storage'; + nodeResources = [ + { + id: 'host-esxi-01', + type: 'agent', + name: 'esxi-01', + displayName: 'esxi-01', + platformId: 'vcenter-main', + platformType: 'vmware-vsphere', + sourceType: 'api', + status: 'online', + uptime: 1000, + lastSeen: Date.now(), + platformData: { sources: ['vmware-vsphere'] }, + } as Resource, + { + id: 'host-esxi-02', + type: 'agent', + name: 'esxi-02', + displayName: 'esxi-02', + platformId: 'vcenter-main', + platformType: 'vmware-vsphere', + sourceType: 'api', + status: 'online', + uptime: 1000, + lastSeen: Date.now(), + platformData: { sources: ['vmware-vsphere'] }, + } as Resource, + ]; hookResources = [ buildStorageResource('vsphere-datastore', 'vsan-prod', 'esxi-01', { platformType: 'vmware-vsphere', @@ -1900,6 +1937,16 @@ describe('Storage', () => { nodes: ['esxi-01'], }, }), + buildStorageResource('vsphere-archive', 'archive-cold', 'esxi-02', { + platformType: 'vmware-vsphere', + parentName: 'esxi-02', + storage: { + platform: 'vmware-vsphere', + type: 'datastore', + topology: 'datastore', + nodes: ['esxi-02'], + }, + }), ]; render(() => ( @@ -1908,6 +1955,7 @@ describe('Storage', () => { tableOnly showFilterToolbar forcedSourceFilter="vmware-vsphere" + suppressNodeFilter filterAriaLabel="vSphere datastore filters" filterSearchPlaceholder="Search vSphere datastores by name, host, or capacity group" filterSearchEmptyMessage="Recent vSphere datastore searches appear here." @@ -1918,6 +1966,15 @@ describe('Storage', () => { expect( screen.getByPlaceholderText('Search vSphere datastores by name, host, or capacity group'), ).toBeInTheDocument(); + expect(screen.queryByRole('combobox', { name: 'Filter' })).not.toBeInTheDocument(); + + fireEvent.input( + screen.getByPlaceholderText('Search vSphere datastores by name, host, or capacity group'), + { target: { value: 'esxi-02' } }, + ); + + expect(screen.queryByText('vsan-prod')).not.toBeInTheDocument(); + expect(screen.getByText('archive-cold')).toBeInTheDocument(); }); it('supports Proxmox platform table embedding without standalone page chrome', async () => { diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index 2001495768..fbbd628876 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -156,12 +156,15 @@ describe('platform overview layout guardrails', () => { expect(kubernetesPageSurfaceSource).toContain(' { diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx index 24e31283a8..2e2235dc72 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx @@ -150,6 +150,7 @@ export function ProxmoxPageSurface() { tableOnly showFilterToolbar forcedSourceFilter={PROXMOX_PLATFORM_FILTER} + suppressNodeFilter filterAriaLabel="Proxmox storage filters" filterSearchPlaceholder="Search Proxmox storage by pool, datastore, node, or device" filterSearchEmptyMessage="Recent Proxmox storage searches appear here." diff --git a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx index 91c8ed5395..532a4f1482 100644 --- a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx +++ b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx @@ -98,6 +98,7 @@ export function TrueNASPageSurface() { tableOnly showFilterToolbar forcedSourceFilter={TRUENAS_PLATFORM_FILTER} + suppressNodeFilter filterAriaLabel="TrueNAS storage filters" filterSearchPlaceholder="Search TrueNAS pools, datasets, disks, or nodes" filterSearchEmptyMessage="Recent TrueNAS storage searches appear here." diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx index 93ac8b2005..3ca1988d84 100644 --- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx +++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx @@ -96,6 +96,7 @@ export function VmwarePageSurface() { tableOnly showFilterToolbar forcedSourceFilter={VMWARE_PLATFORM_FILTER} + suppressNodeFilter filterAriaLabel="vSphere datastore filters" filterSearchPlaceholder="Search vSphere datastores by name, host, or capacity group" filterSearchEmptyMessage="Recent vSphere datastore searches appear here."