mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +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 { Show, createMemo } from 'solid-js';
|
||||
import { WorkloadsSurface } from '@/components/Workloads/WorkloadsSurface';
|
||||
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
|
||||
import {
|
||||
PlatformErrorState,
|
||||
PlatformSectionTabs,
|
||||
PlatformTableEmptyState,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import { DockerHostsTable } from './DockerHostsTable';
|
||||
import { DockerServicesTable } from './DockerServicesTable';
|
||||
import {
|
||||
DOCKER_TAB_SPECS,
|
||||
buildDockerPageModel,
|
||||
buildDockerContainerDefaultHiddenColumnIds,
|
||||
buildVisibleDockerTabSpecs,
|
||||
type DockerPageTabId,
|
||||
} from './dockerPageModel';
|
||||
|
||||
const DOCKER_RESOURCE_QUERY = 'type=agent,docker-host,app-container,docker-service';
|
||||
@@ -27,12 +22,10 @@ const DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES = {
|
||||
context: 'Host',
|
||||
disk: 'Writable layer',
|
||||
} 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" />;
|
||||
|
||||
export function DockerPageSurface() {
|
||||
const location = useLocation();
|
||||
const { resources, loading, error, refetch } = useUnifiedResources({
|
||||
query: DOCKER_RESOURCE_QUERY,
|
||||
cacheKey: 'docker-workspace',
|
||||
@@ -42,24 +35,9 @@ export function DockerPageSurface() {
|
||||
const dockerWorkloadDefaultHiddenColumns = createMemo(() =>
|
||||
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 (
|
||||
<div data-testid="docker-page" class="space-y-3">
|
||||
<PlatformSectionTabs
|
||||
tabs={visibleTabs()}
|
||||
active={activeTab()}
|
||||
ariaLabel="Docker sections"
|
||||
/>
|
||||
|
||||
<Show
|
||||
when={!loading() || model().resources.length > 0}
|
||||
fallback={
|
||||
@@ -90,44 +68,14 @@ export function DockerPageSurface() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show when={activeTab() === 'overview'}>
|
||||
<div class="space-y-4">
|
||||
<DockerHostsTable
|
||||
resources={model().hosts}
|
||||
emptyIcon={dockerIcon()}
|
||||
emptyTitle="No Docker hosts"
|
||||
emptyDescription="Container hosts appear here once a Pulse agent registers them."
|
||||
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'}>
|
||||
<div class="space-y-4">
|
||||
<DockerHostsTable
|
||||
resources={model().hosts}
|
||||
emptyIcon={dockerIcon()}
|
||||
emptyTitle="No Docker hosts"
|
||||
emptyDescription="Container hosts appear here once a Pulse agent registers them."
|
||||
showToolbar={false}
|
||||
/>
|
||||
<WorkloadsSurface
|
||||
vms={[]}
|
||||
containers={[]}
|
||||
@@ -143,16 +91,18 @@ export function DockerPageSurface() {
|
||||
columnVisibilityStorageScope={DOCKER_WORKLOAD_COLUMN_SCOPE}
|
||||
additionalDefaultHiddenColumnIds={dockerWorkloadDefaultHiddenColumns()}
|
||||
columnLabelOverrides={DOCKER_WORKLOAD_COLUMN_LABEL_OVERRIDES}
|
||||
compactGroupHeaders
|
||||
/>
|
||||
</Show>
|
||||
<Show when={activeTab() === 'services'}>
|
||||
<DockerServicesTable
|
||||
resources={model().services}
|
||||
emptyIcon={dockerIcon()}
|
||||
emptyTitle="No Swarm services"
|
||||
emptyDescription="Docker Swarm services appear here when a Swarm manager reports them."
|
||||
/>
|
||||
</Show>
|
||||
<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>
|
||||
</Show>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import {
|
||||
DOCKER_TAB_SPECS,
|
||||
buildDockerContainerDefaultHiddenColumnIds,
|
||||
buildDockerPageModel,
|
||||
buildVisibleDockerTabSpecs,
|
||||
hasDockerSwarmEvidence,
|
||||
} from '../dockerPageModel';
|
||||
|
||||
@@ -20,14 +18,6 @@ const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const model = buildDockerPageModel([
|
||||
makeResource({ id: 'docker-host-1', type: 'agent' }),
|
||||
@@ -64,32 +54,6 @@ describe('dockerPageModel', () => {
|
||||
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', () => {
|
||||
expect(
|
||||
buildDockerContainerDefaultHiddenColumnIds([
|
||||
|
||||
@@ -1,20 +1,6 @@
|
||||
import { resolveResourcePlatformType } from '@/utils/sourcePlatforms';
|
||||
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_CONTAINER_TYPES = new Set<ResourceType>(['app-container']);
|
||||
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[] = [
|
||||
{
|
||||
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',
|
||||
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 ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
@@ -178,8 +184,6 @@ test.describe('Platform pages shell', () => {
|
||||
// own canonical FilterBar via `showFilterToolbar`.
|
||||
const cases: ReadonlyArray<{ path: string; testId: string }> = [
|
||||
{ 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/nodes', testId: 'kubernetes-page' },
|
||||
{ path: '/kubernetes/pods', testId: 'kubernetes-page' },
|
||||
|
||||
Reference in New Issue
Block a user