Fix Docker overview drawer interactions

This commit is contained in:
rcourtman
2026-05-19 20:08:58 +01:00
parent cd2be53725
commit 423beeb903
8 changed files with 190 additions and 5 deletions
@@ -49,6 +49,7 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
<div class="mb-1 flex items-center justify-between gap-3 border-b border-border px-1">
<div class="flex items-center gap-6">
<button
type="button"
onClick={() => switchTab('overview')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'overview' ? 'text-blue-600 dark:text-blue-400' : ' hover:text-muted'
@@ -61,6 +62,7 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
</button>
{hasHistorySupport() && (
<button
type="button"
onClick={() => switchTab('history')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'history' ? 'text-blue-600 dark:text-blue-400' : ' hover:text-muted'
@@ -74,6 +76,7 @@ export const GuestDrawer: Component<GuestDrawerProps> = (props) => {
)}
{hasDiscoverySupport() && (
<button
type="button"
onClick={() => switchTab('discovery')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'discovery'
@@ -40,6 +40,7 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
<div class="mb-1 flex items-center justify-between gap-3 border-b border-border px-1">
<div class="flex items-center gap-6">
<button
type="button"
onClick={() => setActiveTab('overview')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'overview' ? 'text-blue-600 dark:text-blue-400' : ' hover:text-muted'
@@ -51,6 +52,7 @@ export const NodeDrawer: Component<NodeDrawerProps> = (props) => {
)}
</button>
<button
type="button"
onClick={() => setActiveTab('history')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'history' ? 'text-blue-600 dark:text-blue-400' : ' hover:text-muted'
@@ -8,6 +8,7 @@ import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar';
import {
GROUPED_TABLE_ROW_BADGE_CLASS,
getGroupedTableRowCellClass,
getGroupedTableRowClass,
getInteractiveGroupedTableRowClass,
} from '@/components/shared/groupedTableRowPresentation';
import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader';
@@ -418,14 +419,18 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
when={node()}
fallback={
<TableRow
class={getInteractiveGroupedTableRowClass()}
class={
canOpenNodeDrawer()
? getInteractiveGroupedTableRowClass()
: getGroupedTableRowClass()
}
data-summary-group-id={groupKey()}
data-summary-group-series-count={String(
groupSummaryScope()?.seriesIds.length ?? 0,
)}
data-summary-row-active={isSummaryGroupHighlighted() ? 'true' : 'false'}
onClick={handleGroupFocusToggle}
{...groupRowInteraction}
onClick={canOpenNodeDrawer() ? handleGroupFocusToggle : undefined}
{...(canOpenNodeDrawer() ? groupRowInteraction : {})}
>
<TableCell
colspan={props.totalColumns()}
@@ -486,7 +491,7 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
),
'data-summary-row-active': isSummaryGroupHighlighted() ? 'true' : 'false',
onClick: canOpenNodeDrawer() ? handleGroupFocusToggle : undefined,
...groupRowInteraction,
...(canOpenNodeDrawer() ? groupRowInteraction : {}),
}}
/>
</Show>
@@ -1030,8 +1030,17 @@ describe('Workloads performance contract', () => {
expect(workloadPanelSource).toContain('buildWorkloadSummaryGroupScope');
expect(workloadPanelSource).toContain('data-summary-group-id');
expect(workloadPanelSource).toContain('setHoveredWorkloadGroupScope');
expect(workloadPanelSource).toContain('getGroupedTableRowClass');
expect(workloadPanelSource).toContain('getInteractiveGroupedTableRowClass');
expect(workloadPanelSource).toContain('getGroupedTableRowCellClass');
expect(workloadPanelSource).toContain('? getInteractiveGroupedTableRowClass()');
expect(workloadPanelSource).toContain(': getGroupedTableRowClass()');
expect(workloadPanelSource).toContain(
'onClick={canOpenNodeDrawer() ? handleGroupFocusToggle : undefined}',
);
expect(workloadPanelSource).toContain(
'{...(canOpenNodeDrawer() ? groupRowInteraction : {})}',
);
expect(workloadPanelSource).not.toContain('cursor-pointer bg-surface-alt');
expect(workloadsWorkloadTableSource).not.toContain(
'createMemo(() => getCanonicalWorkloadId(guest()))',
@@ -43,6 +43,7 @@ export const DockerHostDrawer: Component<DockerHostDrawerProps> = (props) => {
<div class="mb-1 flex items-center justify-between gap-3 border-b border-border px-1">
<div class="flex items-center gap-6">
<button
type="button"
onClick={() => setActiveTab('overview')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'overview' ? 'text-blue-600 dark:text-blue-400' : ' hover:text-muted'
@@ -54,6 +55,7 @@ export const DockerHostDrawer: Component<DockerHostDrawerProps> = (props) => {
)}
</button>
<button
type="button"
onClick={() => setActiveTab('history')}
class={`pb-2 text-sm font-medium transition-colors relative ${
activeTab() === 'history' ? 'text-blue-600 dark:text-blue-400' : ' hover:text-muted'
@@ -68,6 +68,7 @@ export function DockerPageSurface() {
columnLabelOverrides: DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES,
groupLabelBadges: dockerWorkloadGroupLabelBadges(),
compactGroupHeaders: true,
groupNodeDrawerMode: 'disabled',
});
const pageFilters = createMemo(() => ({
containerRuntime: workloadsState.containerRuntime().trim() || null,
@@ -178,6 +179,7 @@ export function DockerPageSurface() {
tableOnly
forcedPlatform={DOCKER_PLATFORM_FILTER}
forcedViewMode={DOCKER_WORKLOAD_FORCED_VIEW_MODE}
groupNodeDrawerMode="disabled"
emptyStateTitle="No Docker or Podman containers"
emptyStateDescription="Containers appear here when a Docker or Podman host reports workload inventory."
/>
@@ -1,4 +1,4 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Disk } from '@/types/api';
@@ -99,6 +99,32 @@ describe('DockerHostsTable', () => {
expect(screen.getByTestId('stacked-disk-bar')).toHaveAttribute('data-disks', '2');
});
it('opens host details inline without route navigation or submit-style drawer controls', () => {
window.history.pushState({}, '', '/docker/overview');
render(() => (
<DockerHostsTable
resources={[makeDockerHost()]}
emptyIcon={<span />}
emptyTitle="No Docker hosts"
emptyDescription="No hosts"
showToolbar={false}
/>
));
const hostRow = screen.getByText('docker-01').closest('tr');
expect(hostRow).not.toBeNull();
fireEvent.click(hostRow!);
expect(hostRow).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByTestId('docker-host-drawer')).toBeInTheDocument();
expect(window.location.pathname).toBe('/docker/overview');
expect(window.location.search).toBe('');
expect(screen.getByRole('button', { name: 'Overview' })).toHaveAttribute('type', 'button');
expect(screen.getByRole('button', { name: 'History' })).toHaveAttribute('type', 'button');
});
it('identifies the host system separately from the container runtime', () => {
render(() => (
<DockerHostsTable
@@ -0,0 +1,136 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Resource } from '@/types/resource';
import { DockerPageSurface } from '../DockerPageSurface';
const mocks = vi.hoisted(() => ({
useUnifiedResources: vi.fn(),
useWorkloadsState: vi.fn(),
}));
vi.mock('@/hooks/useUnifiedResources', () => ({
useUnifiedResources: mocks.useUnifiedResources,
}));
vi.mock('@/components/Workloads/useWorkloadsState', () => ({
useWorkloadsState: mocks.useWorkloadsState,
}));
vi.mock('@/components/Workloads/WorkloadsSurface', () => ({
WorkloadsSurface: (props: {
groupNodeDrawerMode?: string;
state?: { groupNodeDrawerMode?: () => string };
}) => (
<div
data-testid="docker-workloads-surface"
data-group-node-drawer-mode={props.groupNodeDrawerMode ?? ''}
data-state-group-node-drawer-mode={props.state?.groupNodeDrawerMode?.() ?? ''}
/>
),
}));
const makeDockerHost = (overrides: Partial<Resource> = {}): Resource => ({
id: 'agent:docker-01',
name: 'docker-01',
displayName: 'docker-01',
platformId: 'lab',
platformType: 'docker',
sourceType: 'agent',
status: 'online',
type: 'agent',
lastSeen: 1_700_000_000_000,
docker: {
runtime: 'docker',
runtimeVersion: '27.5.1',
containerCount: 4,
} as NonNullable<Resource['docker']>,
...overrides,
});
const makeDockerContainer = (overrides: Partial<Resource> = {}): Resource => ({
id: 'app-container:docker-01:web',
name: 'web',
displayName: 'web',
platformId: 'lab',
platformType: 'docker',
sourceType: 'agent',
status: 'running',
type: 'app-container',
lastSeen: 1_700_000_000_000,
docker: {
runtime: 'docker',
hostname: 'docker-01',
} as NonNullable<Resource['docker']>,
...overrides,
});
const makeWorkloadsState = (props: { groupNodeDrawerMode?: 'inline' | 'disabled' }) =>
({
allGuests: () => [],
clearPinnedSummaryScope: vi.fn(),
containerRuntime: () => '',
containerRuntimeFilterConfig: () => undefined,
focusedSummaryWorkloadGroupId: () => null,
groupNodeDrawerMode: () => props.groupNodeDrawerMode ?? 'inline',
groupingMode: () => 'grouped',
handleBeforeAutoFocus: vi.fn(),
hostFilterConfig: () => undefined,
search: () => '',
selectedGuestId: () => null,
selectedNode: () => null,
setGroupingMode: vi.fn(),
setMetricDisplayMode: vi.fn(),
setSearch: vi.fn(),
setSortDirection: vi.fn(),
setSortKey: vi.fn(),
setStatusMode: vi.fn(),
setViewMode: vi.fn(),
setWorkloadMetricHistoryRange: vi.fn(),
statusMode: () => 'all',
surfaceConnected: () => false,
surfaceInitialDataReceived: () => false,
viewMode: () => 'app-container',
workloadMetricDisplayMode: () => 'bars',
workloadMetricHistoryRange: () => '1h',
workloadsFilterColumnVisibility: () => undefined,
}) as const;
beforeEach(() => {
mocks.useUnifiedResources.mockReturnValue({
error: () => null,
loading: () => false,
refetch: vi.fn(),
resources: () => [makeDockerHost(), makeDockerContainer()],
});
mocks.useWorkloadsState.mockImplementation(makeWorkloadsState);
});
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
describe('DockerPageSurface', () => {
it('keeps host drawer ownership on the Docker hosts table instead of workload groups', () => {
render(() => <DockerPageSurface />);
expect(mocks.useWorkloadsState).toHaveBeenCalledWith(
expect.objectContaining({
compactGroupHeaders: true,
forcedPlatform: 'docker',
forcedViewMode: 'app-container',
groupNodeDrawerMode: 'disabled',
tableOnly: true,
}),
);
expect(screen.getByTestId('docker-workloads-surface')).toHaveAttribute(
'data-group-node-drawer-mode',
'disabled',
);
expect(screen.getByTestId('docker-workloads-surface')).toHaveAttribute(
'data-state-group-node-drawer-mode',
'disabled',
);
});
});