mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Put storage view controls in table header
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user