mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 08:33:03 +00:00
Align platform storage filters with overview search
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
+3
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user