Align platform storage filters with overview search

This commit is contained in:
rcourtman
2026-05-20 16:56:09 +01:00
parent 35399389a9
commit f58a5f3290
7 changed files with 91 additions and 11 deletions
@@ -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<StorageProps> = (props) => {
}
});
createEffect(() => {
if (!props.suppressNodeFilter) return;
if (selectedNodeId() !== DEFAULT_STORAGE_SELECTED_NODE_ID) {
setSelectedNodeId(DEFAULT_STORAGE_SELECTED_NODE_ID);
}
});
return (
<div ref={setClearSurfaceRootRef} class="space-y-4" data-testid="storage-page">
<Show when={!props.embedded}>
@@ -204,6 +213,7 @@ const Storage: Component<StorageProps> = (props) => {
setSourceFilter={setSourceFilter}
sourceOptions={sourceFilterOptions}
suppressSourceFilter={props.suppressSourceFilter || Boolean(props.forcedSourceFilter)}
suppressNodeFilter={props.suppressNodeFilter}
diskRoleFilter={diskRoleFilter}
setDiskRoleFilter={setDiskRoleFilter}
diskRoleOptions={diskRoleOptions}
@@ -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<StoragePageControlsProps> = (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<StoragePageControlsProps> = (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<StoragePageControlsProps> = (props)
value: option.value,
label: option.label,
})),
},
];
});
}
if (isPoolsView()) {
filters.push({
@@ -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 () => {
@@ -156,12 +156,15 @@ describe('platform overview layout guardrails', () => {
expect(kubernetesPageSurfaceSource).toContain('<KubernetesDeploymentsTable');
expect(truenasPageSurfaceSource).toContain('<TrueNASSystemsTable');
expect(truenasPageSurfaceSource).toContain('<StorageSurface');
expect(truenasPageSurfaceSource).toContain('suppressNodeFilter');
expect(truenasPageSurfaceSource).not.toContain('forcedView="pools"');
expect(truenasPageSurfaceSource).not.toContain('<TrueNASDisksTable');
expect(vmwarePageSurfaceSource).toContain('<VsphereHostsTable');
expect(vmwarePageSurfaceSource).toContain('<WorkloadsSurface');
expect(vmwarePageSurfaceSource).toContain('<StorageSurface');
expect(vmwarePageSurfaceSource).toContain('suppressNodeFilter');
expect(vmwarePageSurfaceSource).not.toContain('forcedView="pools"');
expect(proxmoxPageSurfaceSource).toContain('suppressNodeFilter');
});
it('keeps secondary overview tables from rendering duplicate standalone toolbars', () => {
@@ -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."
@@ -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."
@@ -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."