mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 12:42:49 +00:00
Move Proxmox host drawers to the host table
This commit is contained in:
@@ -309,7 +309,10 @@ regression protection.
|
||||
`WorkloadPanel` owns the mutually exclusive host/guest drawer handoff:
|
||||
clicking a grouped host row while a guest drawer is open must clear the
|
||||
selected guest and keep/focus the host summary group so the node drawer can
|
||||
replace the guest drawer.
|
||||
replace the guest drawer. Platform pages that render a dedicated host
|
||||
table above an embedded Workloads table may disable the grouped host drawer
|
||||
through the explicit Workloads surface option, but standalone Workloads
|
||||
must keep the inline grouped-row drawer as the default.
|
||||
Compact icon headers inside `WorkloadTableHeader.tsx` may stay visually
|
||||
dense for responsive workload tables, but the icon must be decorative and
|
||||
the column label must remain present through an `sr-only` label so the
|
||||
|
||||
@@ -14,6 +14,7 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
|
||||
if (!Number.isFinite(parsed)) return '0';
|
||||
return String(Math.max(0, Math.min(parsed, 100)));
|
||||
};
|
||||
const invertPercent = (value: number) => String(100 - Math.max(0, Math.min(value, 100)));
|
||||
|
||||
return (
|
||||
<div ref={state.setContainerRef} class={presentation().containerClass}>
|
||||
@@ -204,9 +205,25 @@ export function StackedDiskBar(props: StackedDiskBarProps) {
|
||||
>
|
||||
<div
|
||||
data-stacked-disk-fill="vertical"
|
||||
class="absolute bottom-0 left-0 right-0 rounded-sm brightness-100 transition-[filter] duration-100 group-hover/disk:brightness-125"
|
||||
style={{ height: `${bar.fillPercent}%`, background: bar.color }}
|
||||
/>
|
||||
class="absolute inset-0 rounded-sm brightness-100 transition-[filter] duration-100 group-hover/disk:brightness-125"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 h-full w-full"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<rect
|
||||
class="metric-fill-geometry"
|
||||
x="0"
|
||||
y={invertPercent(bar.fillPercent)}
|
||||
width="100"
|
||||
height={clampPercent(bar.fillPercent)}
|
||||
rx="2"
|
||||
fill={bar.color}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -43,6 +43,7 @@ type WorkloadPanelProps = Pick<
|
||||
| 'groupedWindowing'
|
||||
| 'guestMetadata'
|
||||
| 'guestParentNodeMap'
|
||||
| 'groupNodeDrawerMode'
|
||||
| 'groupingMode'
|
||||
| 'handleCustomUrlUpdate'
|
||||
| 'handleTagClick'
|
||||
@@ -380,14 +381,17 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
|
||||
);
|
||||
const shouldShowNodeDrawer = createMemo(
|
||||
() =>
|
||||
props.groupNodeDrawerMode() === 'inline' &&
|
||||
Boolean(node()) &&
|
||||
props.focusedSummaryWorkloadGroupId() === groupKey() &&
|
||||
props.selectedGuestId() === null,
|
||||
);
|
||||
const canOpenNodeDrawer = () => props.groupNodeDrawerMode() === 'inline';
|
||||
const handleGroupHoverChange = (next: SummarySeriesGroupScope | null) => {
|
||||
props.setHoveredWorkloadGroupScope(next);
|
||||
};
|
||||
const handleGroupFocusToggle = () => {
|
||||
if (!canOpenNodeDrawer()) return;
|
||||
const scope = groupSummaryScope();
|
||||
const selectedGuestId = props.selectedGuestId();
|
||||
const nextFocusedScope =
|
||||
@@ -445,7 +449,9 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
|
||||
node={node()!}
|
||||
renderAs="tr"
|
||||
colspan={props.totalColumns()}
|
||||
columns={props.compactGroupHeaders() ? undefined : props.workloadTableVisibleColumns()}
|
||||
columns={
|
||||
props.compactGroupHeaders() ? undefined : props.workloadTableVisibleColumns()
|
||||
}
|
||||
columnCellClass={
|
||||
props.compactGroupHeaders() ? undefined : getGroupNodeColumnCellClass
|
||||
}
|
||||
@@ -456,15 +462,23 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
|
||||
!props.compactGroupHeaders() &&
|
||||
!props.workloadTableVisibleColumnIds().includes('info')
|
||||
}
|
||||
trClass="cursor-pointer select-none duration-150"
|
||||
trClass={
|
||||
canOpenNodeDrawer()
|
||||
? 'cursor-pointer select-none duration-150'
|
||||
: 'select-none duration-150'
|
||||
}
|
||||
trProps={{
|
||||
'aria-expanded': shouldShowNodeDrawer() ? 'true' : 'false',
|
||||
'aria-expanded': canOpenNodeDrawer()
|
||||
? shouldShowNodeDrawer()
|
||||
? 'true'
|
||||
: 'false'
|
||||
: undefined,
|
||||
'data-summary-group-id': groupKey(),
|
||||
'data-summary-group-series-count': String(
|
||||
groupSummaryScope()?.seriesIds.length ?? 0,
|
||||
),
|
||||
'data-summary-row-active': isSummaryGroupHighlighted() ? 'true' : 'false',
|
||||
onClick: handleGroupFocusToggle,
|
||||
onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined,
|
||||
...groupRowInteraction,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -147,6 +147,7 @@ export function WorkloadsSurface(props: WorkloadsSurfaceProps) {
|
||||
groupedWindowing={state.groupedWindowing}
|
||||
guestMetadata={state.guestMetadata}
|
||||
guestParentNodeMap={state.guestParentNodeMap}
|
||||
groupNodeDrawerMode={state.groupNodeDrawerMode}
|
||||
groupingMode={state.groupingMode}
|
||||
handleCustomUrlUpdate={state.handleCustomUrlUpdate}
|
||||
handleSort={state.handleSort}
|
||||
|
||||
@@ -20,6 +20,7 @@ type WorkloadsTableProps = Pick<
|
||||
| 'groupedWindowing'
|
||||
| 'guestMetadata'
|
||||
| 'guestParentNodeMap'
|
||||
| 'groupNodeDrawerMode'
|
||||
| 'groupingMode'
|
||||
| 'handleCustomUrlUpdate'
|
||||
| 'handleSort'
|
||||
@@ -103,6 +104,7 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
|
||||
groupedWindowing={props.groupedWindowing}
|
||||
guestMetadata={props.guestMetadata}
|
||||
guestParentNodeMap={props.guestParentNodeMap}
|
||||
groupNodeDrawerMode={props.groupNodeDrawerMode}
|
||||
groupingMode={props.groupingMode}
|
||||
handleCustomUrlUpdate={props.handleCustomUrlUpdate}
|
||||
handleTagClick={props.handleTagClick}
|
||||
|
||||
+38
-19
@@ -370,7 +370,9 @@ describe('Workloads platform-page embed contract', () => {
|
||||
expect(stateSource).toContain(
|
||||
'onMetricDisplayModeChange?: (value: WorkloadsMetricDisplayMode) => void;',
|
||||
);
|
||||
expect(stateSource).toContain('metricHistoryRange?: Accessor<WorkloadTableMetricHistoryRange>;');
|
||||
expect(stateSource).toContain(
|
||||
'metricHistoryRange?: Accessor<WorkloadTableMetricHistoryRange>;',
|
||||
);
|
||||
expect(stateSource).toContain(
|
||||
'onMetricHistoryRangeChange?: (value: WorkloadTableMetricHistoryRange) => void;',
|
||||
);
|
||||
@@ -381,17 +383,12 @@ describe('Workloads platform-page embed contract', () => {
|
||||
// The controls layer must short-circuit to the page-provided accessor +
|
||||
// change handler when supplied; the internal persistent signal stays as
|
||||
// the fallback so standalone usage keeps working.
|
||||
expect(controlsSource).toContain(
|
||||
'options.metricDisplayMode ?? internalMetricDisplayMode',
|
||||
);
|
||||
expect(controlsSource).toContain('options.metricDisplayMode ?? internalMetricDisplayMode');
|
||||
expect(controlsSource).toContain('options.onMetricDisplayModeChange');
|
||||
expect(controlsSource).toContain(
|
||||
'options.metricHistoryRange ?? internalMetricHistoryRange',
|
||||
);
|
||||
expect(controlsSource).toContain('options.metricHistoryRange ?? internalMetricHistoryRange');
|
||||
|
||||
const proxmoxSource = (
|
||||
await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw')
|
||||
).default;
|
||||
const proxmoxSource = (await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw'))
|
||||
.default;
|
||||
expect(proxmoxSource).toContain('STORAGE_KEYS.WORKLOADS_METRIC_DISPLAY_MODE');
|
||||
expect(proxmoxSource).toContain('STORAGE_KEYS.WORKLOADS_METRIC_HISTORY_RANGE');
|
||||
expect(proxmoxSource).toContain('metricDisplayMode={metricDisplayMode}');
|
||||
@@ -399,9 +396,8 @@ describe('Workloads platform-page embed contract', () => {
|
||||
expect(proxmoxSource).toContain('metricHistoryRange={metricHistoryRange}');
|
||||
expect(proxmoxSource).toContain('onMetricHistoryRangeChange={setMetricHistoryRange}');
|
||||
|
||||
const nodesTableSource = (
|
||||
await import('../../../features/proxmox/ProxmoxNodesTable.tsx?raw')
|
||||
).default;
|
||||
const nodesTableSource = (await import('../../../features/proxmox/ProxmoxNodesTable.tsx?raw'))
|
||||
.default;
|
||||
expect(nodesTableSource).toContain('useWorkloadTableMetricHistory');
|
||||
expect(nodesTableSource).toContain('MetricMiniSparkline');
|
||||
expect(nodesTableSource).toContain('isSparklineMode');
|
||||
@@ -410,9 +406,7 @@ describe('Workloads platform-page embed contract', () => {
|
||||
it('exposes compactGroupHeaders so platform pages can strip duplicate host stats from group rows', async () => {
|
||||
const stateSource = (await import('../useWorkloadsState.ts?raw')).default;
|
||||
expect(stateSource).toContain('compactGroupHeaders?: boolean;');
|
||||
expect(stateSource).toContain(
|
||||
'compactGroupHeaders: () => props.compactGroupHeaders === true,',
|
||||
);
|
||||
expect(stateSource).toContain('compactGroupHeaders: () => props.compactGroupHeaders === true,');
|
||||
|
||||
const tableSource = (await import('../WorkloadsTable.tsx?raw')).default;
|
||||
expect(tableSource).toContain(`| 'compactGroupHeaders'`);
|
||||
@@ -430,11 +424,36 @@ describe('Workloads platform-page embed contract', () => {
|
||||
);
|
||||
expect(panelSource).toContain('!props.compactGroupHeaders() &&');
|
||||
|
||||
const proxmoxSource = (
|
||||
await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw')
|
||||
).default;
|
||||
const proxmoxSource = (await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw'))
|
||||
.default;
|
||||
expect(proxmoxSource).toContain('compactGroupHeaders');
|
||||
});
|
||||
|
||||
it('lets platform pages move grouped host drawers to a dedicated host table owner', async () => {
|
||||
const stateSource = (await import('../useWorkloadsState.ts?raw')).default;
|
||||
expect(stateSource).toContain("groupNodeDrawerMode?: 'inline' | 'disabled';");
|
||||
expect(stateSource).toContain(
|
||||
"groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',",
|
||||
);
|
||||
|
||||
const tableSource = (await import('../WorkloadsTable.tsx?raw')).default;
|
||||
expect(tableSource).toContain(`| 'groupNodeDrawerMode'`);
|
||||
expect(tableSource).toContain('groupNodeDrawerMode={props.groupNodeDrawerMode}');
|
||||
|
||||
const panelSource = (await import('../WorkloadPanel.tsx?raw')).default;
|
||||
expect(panelSource).toContain("props.groupNodeDrawerMode() === 'inline'");
|
||||
expect(panelSource).toContain(
|
||||
'onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined',
|
||||
);
|
||||
|
||||
const proxmoxPageSource = (await import('../../../features/proxmox/ProxmoxPageSurface.tsx?raw'))
|
||||
.default;
|
||||
const proxmoxNodesSource = (await import('../../../features/proxmox/ProxmoxNodesTable.tsx?raw'))
|
||||
.default;
|
||||
expect(proxmoxPageSource).toContain('groupNodeDrawerMode="disabled"');
|
||||
expect(proxmoxNodesSource).toContain('NodeDrawer');
|
||||
expect(proxmoxNodesSource).toContain('data-inline-node-detail-for={node.id}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workloads performance contract', () => {
|
||||
|
||||
@@ -79,6 +79,10 @@ export interface WorkloadsSurfaceProps {
|
||||
// `compactGroupHeaders` strips those stats from the NodeGroupHeader rows
|
||||
// in grouped mode so the section dividers don't duplicate the info.
|
||||
compactGroupHeaders?: boolean;
|
||||
// Default Workloads behavior owns grouped host row drawers inline. Platform
|
||||
// pages with a dedicated host table can disable that drawer so host details
|
||||
// open from the host-owned table instead of the embedded guest table.
|
||||
groupNodeDrawerMode?: 'inline' | 'disabled';
|
||||
// When a platform page owns the metric display mode + sparkline range
|
||||
// (so the same toggle drives both the page's hosts table and this
|
||||
// embedded workloads surface), pass the accessors + change handlers.
|
||||
@@ -454,9 +458,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
|
||||
navigate,
|
||||
nodeByInstance,
|
||||
namespaceFilterConfig,
|
||||
platformFilterConfig: props.suppressPlatformFilter
|
||||
? () => undefined
|
||||
: platformFilterConfig,
|
||||
platformFilterConfig: props.suppressPlatformFilter ? () => undefined : platformFilterConfig,
|
||||
platformOptions,
|
||||
reconnect,
|
||||
reconnectSurface,
|
||||
@@ -522,6 +524,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
|
||||
ws,
|
||||
groupingMode,
|
||||
compactGroupHeaders: () => props.compactGroupHeaders === true,
|
||||
groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { For, Show, createMemo, type Accessor, type Component, type JSX } from 'solid-js';
|
||||
import {
|
||||
For,
|
||||
Show,
|
||||
createMemo,
|
||||
createSignal,
|
||||
type Accessor,
|
||||
type Component,
|
||||
type JSX,
|
||||
} from 'solid-js';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { ResponsiveMetricCell } from '@/components/shared/responsive';
|
||||
import { TableCard } from '@/components/shared/TableCard';
|
||||
import { NodeDrawer } from '@/components/Workloads/NodeDrawer';
|
||||
import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
|
||||
import { StackedDiskBar } from '@/components/Workloads/StackedDiskBar';
|
||||
import { MetricMiniSparkline } from '@/components/Workloads/MetricMiniSparkline';
|
||||
@@ -33,6 +42,7 @@ import { type WorkloadsMetricDisplayMode } from '@/components/Workloads/workload
|
||||
import { type WorkloadTableMetricHistoryRange } from '@/components/Workloads/workloadMetricHistoryModel';
|
||||
import type { Disk, Node as LegacyNode } from '@/types/api';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { nodeFromResource } from '@/utils/resourceStateAdapters';
|
||||
import {
|
||||
getResourceClusterLabel,
|
||||
getResourceNodeName,
|
||||
@@ -128,6 +138,7 @@ export const ProxmoxNodesTable: Component<{
|
||||
emptyDescription: string;
|
||||
}> = (props) => {
|
||||
const breakpoint = useBreakpoint();
|
||||
const [selectedNodeId, setSelectedNodeId] = createSignal<string | null>(null);
|
||||
const layoutMode = createMemo(() => getWorkloadTableLayoutMode(breakpoint.width()));
|
||||
const visibleColumns = createMemo(() => getProxmoxHostVisibleColumnsForLayout(layoutMode()));
|
||||
const visibleColumnIds = createMemo(() => visibleColumns().map((column) => column.id));
|
||||
@@ -183,6 +194,18 @@ export const ProxmoxNodesTable: Component<{
|
||||
<For each={props.nodes}>
|
||||
{(node) => {
|
||||
const name = () => asTrimmedString(node.name) || node.id;
|
||||
const drawerNode = () => nodeFromResource(node);
|
||||
const detailRowId = () => `proxmox-host-drawer-${node.id}`;
|
||||
const isSelected = () => selectedNodeId() === node.id;
|
||||
const toggleNodeDrawer = () =>
|
||||
setSelectedNodeId((current) => (current === node.id ? null : node.id));
|
||||
const handleActivationKey: JSX.EventHandler<HTMLTableRowElement, KeyboardEvent> = (
|
||||
event,
|
||||
) => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
toggleNodeDrawer();
|
||||
};
|
||||
const version = () => asTrimmedString(getResourceVersion(node));
|
||||
const cluster = () => getResourceClusterLabel(node);
|
||||
const counts = () => countGuestsForNode(props.guests, getResourceNodeName(node));
|
||||
@@ -397,9 +420,42 @@ export const ProxmoxNodesTable: Component<{
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow class="text-[11px] sm:text-xs">
|
||||
<For each={visibleColumns()}>{(column) => renderColumnCell(column)}</For>
|
||||
</TableRow>
|
||||
<>
|
||||
<TableRow
|
||||
class={`cursor-pointer text-[11px] outline-none sm:text-xs ${
|
||||
isSelected() ? 'bg-surface-hover' : ''
|
||||
} focus-visible:ring-2 focus-visible:ring-blue-500/60 focus-visible:ring-offset-1 focus-visible:ring-offset-surface`}
|
||||
aria-controls={isSelected() ? detailRowId() : undefined}
|
||||
aria-expanded={isSelected() ? 'true' : 'false'}
|
||||
data-proxmox-host-row={node.id}
|
||||
onClick={toggleNodeDrawer}
|
||||
onKeyDown={handleActivationKey}
|
||||
tabIndex={0}
|
||||
>
|
||||
<For each={visibleColumns()}>{(column) => renderColumnCell(column)}</For>
|
||||
</TableRow>
|
||||
<Show when={isSelected() && drawerNode()}>
|
||||
{(selectedNode) => (
|
||||
<TableRow data-inline-node-detail-for={node.id}>
|
||||
<TableCell
|
||||
id={detailRowId()}
|
||||
colspan={visibleColumns().length}
|
||||
class="p-0 border-b border-border bg-surface-alt"
|
||||
>
|
||||
<div
|
||||
class="px-2 py-3 sm:px-4 sm:py-4"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<NodeDrawer
|
||||
node={selectedNode()}
|
||||
disks={normalizeDiskArray(node.agent?.disks)}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
|
||||
@@ -145,6 +145,7 @@ export function ProxmoxPageSurface() {
|
||||
suppressPlatformFilter
|
||||
forcedPlatform={PROXMOX_PLATFORM_FILTER}
|
||||
compactGroupHeaders
|
||||
groupNodeDrawerMode="disabled"
|
||||
metricDisplayMode={metricDisplayMode}
|
||||
onMetricDisplayModeChange={setMetricDisplayMode}
|
||||
metricHistoryRange={metricHistoryRange}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { ProxmoxNodesTable } from '../ProxmoxNodesTable';
|
||||
|
||||
const nodeDrawerMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/hooks/useBreakpoint', () => ({
|
||||
useBreakpoint: () => ({
|
||||
width: () => 1280,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/shared/responsive', () => ({
|
||||
ResponsiveMetricCell: () => <div data-testid="responsive-metric-cell" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/StackedMemoryBar', () => ({
|
||||
StackedMemoryBar: () => <div data-testid="stacked-memory-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/StackedDiskBar', () => ({
|
||||
StackedDiskBar: () => <div data-testid="stacked-disk-bar" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/MetricMiniSparkline', () => ({
|
||||
MetricMiniSparkline: () => <div data-testid="metric-mini-sparkline" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/shared/TemperatureGauge', () => ({
|
||||
TemperatureGauge: () => <div data-testid="temperature-gauge" />,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/useWorkloadTableMetricHistory', () => ({
|
||||
useWorkloadTableMetricHistory: () => ({
|
||||
getNodeMetricSeries: () => [],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/NodeDrawer', () => ({
|
||||
NodeDrawer: (props: { node: { name: string } }) => {
|
||||
nodeDrawerMock(props.node);
|
||||
return <div data-testid="node-drawer">{props.node.name}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
const makeNodeResource = (overrides: Partial<Resource> = {}): Resource => ({
|
||||
id: 'agent:pve-node-1',
|
||||
name: 'pve-node-1',
|
||||
displayName: 'pve-node-1',
|
||||
platformId: 'homelab',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'hybrid',
|
||||
status: 'online',
|
||||
type: 'agent',
|
||||
lastSeen: 1_700_000_000_000,
|
||||
cpu: { current: 42 },
|
||||
memory: { total: 8_000, used: 3_200, free: 4_800, current: 40 },
|
||||
disk: { total: 10_000, used: 4_500, free: 5_500, current: 45 },
|
||||
proxmox: {
|
||||
clusterName: 'homelab',
|
||||
nodeName: 'pve-node-1',
|
||||
pveVersion: 'pve-manager/9.1.9/ee7bad0a3d1546c9',
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('ProxmoxNodesTable', () => {
|
||||
it('opens the host details drawer from the host-owned top table row', async () => {
|
||||
render(() => (
|
||||
<ProxmoxNodesTable
|
||||
nodes={[makeNodeResource()]}
|
||||
guests={[]}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No Proxmox VE nodes"
|
||||
emptyDescription="No nodes"
|
||||
/>
|
||||
));
|
||||
|
||||
const row = screen.getByText('pve-node-1').closest('tr');
|
||||
expect(row).toBeTruthy();
|
||||
expect(row).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByTestId('node-drawer')).not.toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(row!);
|
||||
|
||||
expect(row).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByTestId('node-drawer')).toHaveTextContent('pve-node-1');
|
||||
expect(nodeDrawerMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'pve-node-1' }));
|
||||
|
||||
await fireEvent.click(row!);
|
||||
|
||||
expect(row).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.queryByTestId('node-drawer')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import workloadsStateSource from '@/components/Workloads/useWorkloadsState.ts?raw';
|
||||
import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw';
|
||||
import workloadsTableSource from '@/components/Workloads/WorkloadsTable.tsx?raw';
|
||||
import proxmoxNodesTableSource from '../ProxmoxNodesTable.tsx?raw';
|
||||
import proxmoxPageSurfaceSource from '../ProxmoxPageSurface.tsx?raw';
|
||||
|
||||
describe('Proxmox drawer placement contract', () => {
|
||||
it('keeps host details owned by the Proxmox host table instead of the embedded guest table', () => {
|
||||
expect(workloadsStateSource).toContain("groupNodeDrawerMode?: 'inline' | 'disabled';");
|
||||
expect(workloadsStateSource).toContain(
|
||||
"groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',",
|
||||
);
|
||||
expect(workloadsTableSource).toContain(`| 'groupNodeDrawerMode'`);
|
||||
expect(workloadsTableSource).toContain('groupNodeDrawerMode={props.groupNodeDrawerMode}');
|
||||
expect(workloadPanelSource).toContain("props.groupNodeDrawerMode() === 'inline'");
|
||||
expect(workloadPanelSource).toContain(
|
||||
'onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined',
|
||||
);
|
||||
expect(proxmoxPageSurfaceSource).toContain('groupNodeDrawerMode="disabled"');
|
||||
expect(proxmoxNodesTableSource).toContain('NodeDrawer');
|
||||
expect(proxmoxNodesTableSource).toContain('data-inline-node-detail-for={node.id}');
|
||||
});
|
||||
});
|
||||
@@ -13,10 +13,10 @@ describe('proxmoxHostTableModel', () => {
|
||||
|
||||
expect(ids).toEqual(['node', 'cpu', 'memory', 'disk']);
|
||||
expect(getProxmoxHostTableMinWidthClass('mobile')).toBe('min-w-full');
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '38%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '20%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '21%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '21%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('node', 'mobile', ids)).toEqual({ width: '44%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('cpu', 'mobile', ids)).toEqual({ width: '23%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('memory', 'mobile', ids)).toEqual({ width: '23%' });
|
||||
expect(getProxmoxHostColumnWidthStyle('disk', 'mobile', ids)).toEqual({ width: '10%' });
|
||||
});
|
||||
|
||||
it('adds temperature and guest counts before slower-changing metadata on tablet', () => {
|
||||
@@ -50,7 +50,7 @@ describe('proxmoxHostTableModel', () => {
|
||||
expect(getProxmoxHostTableMinWidthClass('compact')).toBe('min-w-[1080px]');
|
||||
expect(getProxmoxHostTableMinWidthClass('wide')).toBe('min-w-[1080px]');
|
||||
expect(getProxmoxHostColumnWidthStyle('cluster', 'compact', compactIds)).toEqual({
|
||||
width: '12%',
|
||||
width: '13.0435%',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user