mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 16:42:15 +00:00
Fix Docker overview drawer interactions
This commit is contained in:
@@ -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>
|
||||
|
||||
+9
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user