mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Collapse Docker page to single unified surface
The "Hosts" tab already rendered the hosts table, the containers WorkloadsSurface, and the Swarm services table together, so the separate Containers and Swarm services sub-tabs were strict subset views of overview. Other platform pages (Proxmox, TrueNAS, Kubernetes) use sub-tabs for genuinely different data per tab; the Docker shape was the odd one out. Drop PlatformSectionTabs from DockerPageSurface and render the unified hosts + containers + services stack unconditionally. Remove the now-dead DOCKER_TAB_SPECS / DockerPageTabId / DockerTabSpec / buildVisibleDockerTabSpecs exports from dockerPageModel. The DockerPage no longer interprets the URL segment, so legacy /docker/overview, /docker/containers, /docker/services bookmarks continue to resolve via the existing /docker/* wildcard route in App.tsx.
This commit is contained in:
@@ -1,21 +1,16 @@
|
|||||||
import { useLocation } from '@solidjs/router';
|
|
||||||
import ContainerIcon from 'lucide-solid/icons/container';
|
import ContainerIcon from 'lucide-solid/icons/container';
|
||||||
import { Show, createMemo } from 'solid-js';
|
import { Show, createMemo } from 'solid-js';
|
||||||
import { WorkloadsSurface } from '@/components/Workloads/WorkloadsSurface';
|
import { WorkloadsSurface } from '@/components/Workloads/WorkloadsSurface';
|
||||||
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
|
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
|
||||||
import {
|
import {
|
||||||
PlatformErrorState,
|
PlatformErrorState,
|
||||||
PlatformSectionTabs,
|
|
||||||
PlatformTableEmptyState,
|
PlatformTableEmptyState,
|
||||||
} from '@/features/platformPage/sharedPlatformPage';
|
} from '@/features/platformPage/sharedPlatformPage';
|
||||||
import { DockerHostsTable } from './DockerHostsTable';
|
import { DockerHostsTable } from './DockerHostsTable';
|
||||||
import { DockerServicesTable } from './DockerServicesTable';
|
import { DockerServicesTable } from './DockerServicesTable';
|
||||||
import {
|
import {
|
||||||
DOCKER_TAB_SPECS,
|
|
||||||
buildDockerPageModel,
|
buildDockerPageModel,
|
||||||
buildDockerContainerDefaultHiddenColumnIds,
|
buildDockerContainerDefaultHiddenColumnIds,
|
||||||
buildVisibleDockerTabSpecs,
|
|
||||||
type DockerPageTabId,
|
|
||||||
} from './dockerPageModel';
|
} from './dockerPageModel';
|
||||||
|
|
||||||
const DOCKER_RESOURCE_QUERY = 'type=agent,docker-host,app-container,docker-service';
|
const DOCKER_RESOURCE_QUERY = 'type=agent,docker-host,app-container,docker-service';
|
||||||
@@ -27,12 +22,10 @@ const DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES = {
|
|||||||
context: 'Host',
|
context: 'Host',
|
||||||
disk: 'Writable layer',
|
disk: 'Writable layer',
|
||||||
} as const;
|
} as const;
|
||||||
const VALID_TABS = new Set<DockerPageTabId>(DOCKER_TAB_SPECS.map((tab) => tab.id));
|
|
||||||
|
|
||||||
const dockerIcon = () => <ContainerIcon class="h-6 w-6 text-slate-400" />;
|
const dockerIcon = () => <ContainerIcon class="h-6 w-6 text-slate-400" />;
|
||||||
|
|
||||||
export function DockerPageSurface() {
|
export function DockerPageSurface() {
|
||||||
const location = useLocation();
|
|
||||||
const { resources, loading, error, refetch } = useUnifiedResources({
|
const { resources, loading, error, refetch } = useUnifiedResources({
|
||||||
query: DOCKER_RESOURCE_QUERY,
|
query: DOCKER_RESOURCE_QUERY,
|
||||||
cacheKey: 'docker-workspace',
|
cacheKey: 'docker-workspace',
|
||||||
@@ -42,24 +35,9 @@ export function DockerPageSurface() {
|
|||||||
const dockerWorkloadDefaultHiddenColumns = createMemo(() =>
|
const dockerWorkloadDefaultHiddenColumns = createMemo(() =>
|
||||||
buildDockerContainerDefaultHiddenColumnIds(model().containers),
|
buildDockerContainerDefaultHiddenColumnIds(model().containers),
|
||||||
);
|
);
|
||||||
const visibleTabs = createMemo(() => buildVisibleDockerTabSpecs(model()));
|
|
||||||
const visibleTabIds = createMemo(
|
|
||||||
() => new Set<DockerPageTabId>(visibleTabs().map((tab) => tab.id)),
|
|
||||||
);
|
|
||||||
const activeTab = createMemo<DockerPageTabId>(() => {
|
|
||||||
const segment = location.pathname.split('/').filter(Boolean)[1] as DockerPageTabId | undefined;
|
|
||||||
if (!segment || !VALID_TABS.has(segment)) return 'overview';
|
|
||||||
return visibleTabIds().has(segment) ? segment : 'overview';
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="docker-page" class="space-y-3">
|
<div data-testid="docker-page" class="space-y-3">
|
||||||
<PlatformSectionTabs
|
|
||||||
tabs={visibleTabs()}
|
|
||||||
active={activeTab()}
|
|
||||||
ariaLabel="Docker sections"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={!loading() || model().resources.length > 0}
|
when={!loading() || model().resources.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -90,44 +68,14 @@ export function DockerPageSurface() {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={activeTab() === 'overview'}>
|
<div class="space-y-4">
|
||||||
<div class="space-y-4">
|
<DockerHostsTable
|
||||||
<DockerHostsTable
|
resources={model().hosts}
|
||||||
resources={model().hosts}
|
emptyIcon={dockerIcon()}
|
||||||
emptyIcon={dockerIcon()}
|
emptyTitle="No Docker hosts"
|
||||||
emptyTitle="No Docker hosts"
|
emptyDescription="Container hosts appear here once a Pulse agent registers them."
|
||||||
emptyDescription="Container hosts appear here once a Pulse agent registers them."
|
showToolbar={false}
|
||||||
showToolbar={false}
|
/>
|
||||||
/>
|
|
||||||
<WorkloadsSurface
|
|
||||||
vms={[]}
|
|
||||||
containers={[]}
|
|
||||||
nodes={[]}
|
|
||||||
useWorkloads
|
|
||||||
embedded
|
|
||||||
tableOnly
|
|
||||||
showFilterToolbar
|
|
||||||
suppressPlatformFilter
|
|
||||||
forcedPlatform={DOCKER_PLATFORM_FILTER}
|
|
||||||
forcedViewMode={DOCKER_WORKLOAD_FORCED_VIEW_MODE}
|
|
||||||
defaultSortKey={DOCKER_WORKLOAD_DEFAULT_SORT_KEY}
|
|
||||||
columnVisibilityStorageScope={DOCKER_WORKLOAD_COLUMN_SCOPE}
|
|
||||||
additionalDefaultHiddenColumnIds={dockerWorkloadDefaultHiddenColumns()}
|
|
||||||
columnLabelOverrides={DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES}
|
|
||||||
compactGroupHeaders
|
|
||||||
/>
|
|
||||||
<Show when={model().services.length > 0}>
|
|
||||||
<DockerServicesTable
|
|
||||||
resources={model().services}
|
|
||||||
emptyIcon={dockerIcon()}
|
|
||||||
emptyTitle="No Swarm services"
|
|
||||||
emptyDescription="Docker Swarm services appear here when a Swarm manager reports them."
|
|
||||||
showToolbar={false}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={activeTab() === 'containers'}>
|
|
||||||
<WorkloadsSurface
|
<WorkloadsSurface
|
||||||
vms={[]}
|
vms={[]}
|
||||||
containers={[]}
|
containers={[]}
|
||||||
@@ -143,16 +91,18 @@ export function DockerPageSurface() {
|
|||||||
columnVisibilityStorageScope={DOCKER_WORKLOAD_COLUMN_SCOPE}
|
columnVisibilityStorageScope={DOCKER_WORKLOAD_COLUMN_SCOPE}
|
||||||
additionalDefaultHiddenColumnIds={dockerWorkloadDefaultHiddenColumns()}
|
additionalDefaultHiddenColumnIds={dockerWorkloadDefaultHiddenColumns()}
|
||||||
columnLabelOverrides={DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES}
|
columnLabelOverrides={DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES}
|
||||||
|
compactGroupHeaders
|
||||||
/>
|
/>
|
||||||
</Show>
|
<Show when={model().services.length > 0}>
|
||||||
<Show when={activeTab() === 'services'}>
|
<DockerServicesTable
|
||||||
<DockerServicesTable
|
resources={model().services}
|
||||||
resources={model().services}
|
emptyIcon={dockerIcon()}
|
||||||
emptyIcon={dockerIcon()}
|
emptyTitle="No Swarm services"
|
||||||
emptyTitle="No Swarm services"
|
emptyDescription="Docker Swarm services appear here when a Swarm manager reports them."
|
||||||
emptyDescription="Docker Swarm services appear here when a Swarm manager reports them."
|
showToolbar={false}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { Resource } from '@/types/resource';
|
import type { Resource } from '@/types/resource';
|
||||||
import {
|
import {
|
||||||
DOCKER_TAB_SPECS,
|
|
||||||
buildDockerContainerDefaultHiddenColumnIds,
|
buildDockerContainerDefaultHiddenColumnIds,
|
||||||
buildDockerPageModel,
|
buildDockerPageModel,
|
||||||
buildVisibleDockerTabSpecs,
|
|
||||||
hasDockerSwarmEvidence,
|
hasDockerSwarmEvidence,
|
||||||
} from '../dockerPageModel';
|
} from '../dockerPageModel';
|
||||||
|
|
||||||
@@ -20,14 +18,6 @@ const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('dockerPageModel', () => {
|
describe('dockerPageModel', () => {
|
||||||
it('declares the Docker section set with hosts, containers, and Swarm services', () => {
|
|
||||||
expect(DOCKER_TAB_SPECS.map((tab) => tab.id)).toEqual([
|
|
||||||
'overview',
|
|
||||||
'containers',
|
|
||||||
'services',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('buckets Docker hosts, containers, and Swarm services from canonical resources', () => {
|
it('buckets Docker hosts, containers, and Swarm services from canonical resources', () => {
|
||||||
const model = buildDockerPageModel([
|
const model = buildDockerPageModel([
|
||||||
makeResource({ id: 'docker-host-1', type: 'agent' }),
|
makeResource({ id: 'docker-host-1', type: 'agent' }),
|
||||||
@@ -64,32 +54,6 @@ describe('dockerPageModel', () => {
|
|||||||
expect(model.resources).toEqual([]);
|
expect(model.resources).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows Docker subtabs only when canonical resource evidence exists', () => {
|
|
||||||
expect(
|
|
||||||
buildVisibleDockerTabSpecs(
|
|
||||||
buildDockerPageModel([makeResource({ id: 'docker-host-1', type: 'agent' })]),
|
|
||||||
).map((tab) => tab.id),
|
|
||||||
).toEqual(['overview']);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
buildVisibleDockerTabSpecs(
|
|
||||||
buildDockerPageModel([
|
|
||||||
makeResource({ id: 'docker-host-1', type: 'agent' }),
|
|
||||||
makeResource({ id: 'ctr-1', type: 'app-container' }),
|
|
||||||
]),
|
|
||||||
).map((tab) => tab.id),
|
|
||||||
).toEqual(['overview', 'containers']);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
buildVisibleDockerTabSpecs(
|
|
||||||
buildDockerPageModel([
|
|
||||||
makeResource({ id: 'docker-host-1', type: 'agent' }),
|
|
||||||
makeResource({ id: 'svc-1', type: 'docker-service' }),
|
|
||||||
]),
|
|
||||||
).map((tab) => tab.id),
|
|
||||||
).toEqual(['overview', 'services']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hides Docker container I/O columns by default when the snapshot has no I/O telemetry', () => {
|
it('hides Docker container I/O columns by default when the snapshot has no I/O telemetry', () => {
|
||||||
expect(
|
expect(
|
||||||
buildDockerContainerDefaultHiddenColumnIds([
|
buildDockerContainerDefaultHiddenColumnIds([
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
import { resolveResourcePlatformType } from '@/utils/sourcePlatforms';
|
import { resolveResourcePlatformType } from '@/utils/sourcePlatforms';
|
||||||
import type { Resource, ResourceType } from '@/types/resource';
|
import type { Resource, ResourceType } from '@/types/resource';
|
||||||
|
|
||||||
export type DockerPageTabId = 'overview' | 'containers' | 'services';
|
|
||||||
|
|
||||||
export type DockerTabSpec = {
|
|
||||||
id: DockerPageTabId;
|
|
||||||
label: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DOCKER_TAB_SPECS: readonly DockerTabSpec[] = [
|
|
||||||
{ id: 'overview', label: 'Hosts', path: '/docker/overview' },
|
|
||||||
{ id: 'containers', label: 'Containers', path: '/docker/containers' },
|
|
||||||
{ id: 'services', label: 'Swarm services', path: '/docker/services' },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const DOCKER_HOST_TYPES = new Set<ResourceType>(['agent', 'docker-host']);
|
const DOCKER_HOST_TYPES = new Set<ResourceType>(['agent', 'docker-host']);
|
||||||
const DOCKER_CONTAINER_TYPES = new Set<ResourceType>(['app-container']);
|
const DOCKER_CONTAINER_TYPES = new Set<ResourceType>(['app-container']);
|
||||||
const DOCKER_SERVICE_TYPES = new Set<ResourceType>(['docker-service']);
|
const DOCKER_SERVICE_TYPES = new Set<ResourceType>(['docker-service']);
|
||||||
@@ -108,15 +94,3 @@ export function buildDockerPageModel(resources: Resource[]): DockerPageModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildVisibleDockerTabSpecs(model: DockerPageModel): DockerTabSpec[] {
|
|
||||||
const visible = new Set<DockerPageTabId>(['overview']);
|
|
||||||
|
|
||||||
if (model.containers.length > 0) {
|
|
||||||
visible.add('containers');
|
|
||||||
}
|
|
||||||
if (model.services.length > 0) {
|
|
||||||
visible.add('services');
|
|
||||||
}
|
|
||||||
|
|
||||||
return DOCKER_TAB_SPECS.filter((tab) => visible.has(tab.id));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -51,14 +51,6 @@ type PlatformPageCase = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PLATFORM_PAGES: readonly PlatformPageCase[] = [
|
const PLATFORM_PAGES: readonly PlatformPageCase[] = [
|
||||||
{
|
|
||||||
id: 'docker',
|
|
||||||
rootPath: '/docker',
|
|
||||||
testId: 'docker-page',
|
|
||||||
ariaLabel: 'Docker sections',
|
|
||||||
tabPaths: ['/docker/overview', '/docker/containers', '/docker/services'],
|
|
||||||
populatedTabPaths: ['/docker/overview', '/docker/containers', '/docker/services'],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'kubernetes',
|
id: 'kubernetes',
|
||||||
rootPath: '/kubernetes',
|
rootPath: '/kubernetes',
|
||||||
@@ -165,6 +157,20 @@ test.describe('Platform pages shell', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('docker page renders as a single unified surface without sub-tabs', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop shell smoke');
|
||||||
|
|
||||||
|
await stubEmptyResources(page);
|
||||||
|
await page.goto('/docker/overview', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const pageRoot = page.getByTestId('docker-page');
|
||||||
|
await expect(pageRoot).toBeVisible({ timeout: 30_000 });
|
||||||
|
|
||||||
|
await expect(page.getByRole('navigation', { name: 'Docker sections' })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('every platform sub-tab exposes v5-style operator controls', async ({
|
test('every platform sub-tab exposes v5-style operator controls', async ({
|
||||||
page,
|
page,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
@@ -178,8 +184,6 @@ test.describe('Platform pages shell', () => {
|
|||||||
// own canonical FilterBar via `showFilterToolbar`.
|
// own canonical FilterBar via `showFilterToolbar`.
|
||||||
const cases: ReadonlyArray<{ path: string; testId: string }> = [
|
const cases: ReadonlyArray<{ path: string; testId: string }> = [
|
||||||
{ path: '/docker/overview', testId: 'docker-page' },
|
{ path: '/docker/overview', testId: 'docker-page' },
|
||||||
{ path: '/docker/containers', testId: 'docker-page' },
|
|
||||||
{ path: '/docker/services', testId: 'docker-page' },
|
|
||||||
{ path: '/kubernetes/overview', testId: 'kubernetes-page' },
|
{ path: '/kubernetes/overview', testId: 'kubernetes-page' },
|
||||||
{ path: '/kubernetes/nodes', testId: 'kubernetes-page' },
|
{ path: '/kubernetes/nodes', testId: 'kubernetes-page' },
|
||||||
{ path: '/kubernetes/pods', testId: 'kubernetes-page' },
|
{ path: '/kubernetes/pods', testId: 'kubernetes-page' },
|
||||||
|
|||||||
Reference in New Issue
Block a user