Put storage view controls in table header

This commit is contained in:
rcourtman
2026-05-15 22:33:55 +01:00
parent fbf9adb419
commit bfaa1aa596
5 changed files with 73 additions and 14 deletions
@@ -1047,8 +1047,8 @@ Storage and Recovery can now be embedded by a platform page in table-only mode
with a forced Proxmox source/platform filter. The embedded mode suppresses
standalone page chrome, summary charts, and full filter chrome, but the
Storage surface must keep the canonical Storage / Physical Disks view selector
available unless the embedding explicitly locks a `forcedView`. The canonical
route-backed filter state, fetch builders, table rendering, and
inside the table header unless the embedding explicitly locks a `forcedView`.
The canonical route-backed filter state, fetch builders, table rendering, and
storage/recovery vocabulary remain owned by the Storage and Recovery surfaces.
Platform pages must compose those owners rather than cloning storage pools,
physical disks, recovery events, or protected-inventory tables under
@@ -2,8 +2,9 @@ import { Component, Show, createEffect } from 'solid-js';
import StorageCephSection from '@/components/Storage/StorageCephSection';
import StorageContentCard from '@/components/Storage/StorageContentCard';
import StoragePageBanners from '@/components/Storage/StoragePageBanners';
import StoragePageControls, { StorageViewSwitcher } from '@/components/Storage/StoragePageControls';
import StoragePageControls from '@/components/Storage/StoragePageControls';
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';
@@ -166,12 +167,6 @@ const Storage: Component<StorageProps> = (props) => {
</Show>
<div class="space-y-4" data-testid="storage-interaction-surface">
<Show when={props.tableOnly && !props.forcedView && !kioskMode()}>
<div data-summary-clear-ignore>
<StorageViewSwitcher view={view} setView={setView} />
</div>
</Show>
<Show when={!props.tableOnly}>
<div data-summary-clear-ignore>
<StoragePageControls
@@ -245,6 +240,11 @@ const Storage: Component<StorageProps> = (props) => {
setHoveredStorageResourceId={setHoveredStorageResourceId}
selectedDiskId={selectedDiskId}
setSelectedDiskId={setSelectedDiskId}
actions={
props.tableOnly && !props.forcedView && !kioskMode() ? (
<StorageViewSegmentedControl value={view()} onChange={setView} />
) : undefined
}
/>
</div>
</div>
@@ -1,4 +1,4 @@
import { Component, Show } from 'solid-js';
import { Component, Show, type JSX } from 'solid-js';
import { TableCardHeader } from '@/components/shared/TableCardHeader';
import { TableCard } from '@/components/shared/TableCard';
import { DiskList } from '@/components/Storage/DiskList';
@@ -48,6 +48,7 @@ type StorageContentCardProps = {
setHoveredStorageResourceId: (value: string | null) => void;
selectedDiskId: () => string | null;
setSelectedDiskId: (value: string | null) => void;
actions?: JSX.Element;
};
export const StorageContentCard: Component<StorageContentCardProps> = (props) => {
@@ -66,6 +67,7 @@ export const StorageContentCard: Component<StorageContentCardProps> = (props) =>
>
<TableCardHeader
title={model.heading()}
actions={props.actions}
showClearAction={showClearSelection()}
onClear={props.clearPinnedSummaryScope}
/>
@@ -0,0 +1,52 @@
import { splitProps, type Component, type JSX } from 'solid-js';
import DatabaseIcon from 'lucide-solid/icons/database';
import HardDriveIcon from 'lucide-solid/icons/hard-drive';
import { FilterSegmentedControl } from '@/components/shared/FilterToolbar';
import type { StorageView } from './storagePageState';
interface StorageViewSegmentedControlProps extends Omit<
JSX.HTMLAttributes<HTMLDivElement>,
'onChange'
> {
value: StorageView;
onChange: (value: StorageView) => void;
}
export const StorageViewSegmentedControl: Component<StorageViewSegmentedControlProps> = (props) => {
const [local, divProps] = splitProps(props, ['value', 'onChange']);
return (
<FilterSegmentedControl
{...divProps}
role={divProps.role ?? 'group'}
aria-label={divProps['aria-label'] ?? 'Storage table view'}
label="View"
value={local.value}
onChange={(value) => local.onChange(value as StorageView)}
options={[
{
value: 'pools',
title: 'Show storage pools and backup targets',
label: (
<>
<DatabaseIcon aria-hidden="true" class="h-3 w-3" />
Storage
</>
),
},
{
value: 'disks',
title: 'Show physical disks',
label: (
<>
<HardDriveIcon aria-hidden="true" class="h-3 w-3" />
Physical Disks
</>
),
},
]}
/>
);
};
export default StorageViewSegmentedControl;
@@ -1834,16 +1834,20 @@ describe('Storage', () => {
render(() => <Storage embedded tableOnly forcedSourceFilter="proxmox-pve" />);
expect(screen.getByRole('tablist', { name: 'Storage view' })).toBeInTheDocument();
const viewOptions = screen.getByRole('group', { name: 'Storage table view' });
expect(within(viewOptions).getByRole('button', { name: 'Storage' })).toHaveAttribute(
'aria-pressed',
'true',
);
expect(screen.queryByTestId('storage-summary')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: 'Physical Disks' }));
fireEvent.click(within(viewOptions).getByRole('button', { name: 'Physical Disks' }));
await waitFor(() => {
expect(screen.getByTestId('disk-list')).toHaveTextContent('disk-view:all:');
});
expect(screen.getByRole('tab', { name: 'Physical Disks' })).toHaveAttribute(
'aria-selected',
expect(within(viewOptions).getByRole('button', { name: 'Physical Disks' })).toHaveAttribute(
'aria-pressed',
'true',
);
});
@@ -1870,6 +1874,7 @@ describe('Storage', () => {
expect(screen.queryByTestId('storage-summary')).not.toBeInTheDocument();
expect(screen.queryByText('Ceph')).not.toBeInTheDocument();
expect(screen.queryByRole('tablist', { name: 'Storage view' })).not.toBeInTheDocument();
expect(screen.queryByRole('group', { name: 'Storage table view' })).not.toBeInTheDocument();
});
it('collapses and restores storage charts from the shared toolbar toggle', async () => {