From 65b069dbba9110250ec79f663334c177d2414a54 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 16 May 2026 00:02:16 +0100 Subject: [PATCH] frontend(platforms): restore v5-style operator controls on embedded canonical surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v5 platform/dashboard pages had a dense filter card with search, status chips, view-mode toggle, grouping toggle, column picker, and sort controls (DashboardFilter.tsx, DockerFilter.tsx, StorageFilter.tsx). v6 platform pages mounted WorkloadsSurface and StorageSurface in `embedded tableOnly` mode, which hid the entire canonical filter toolbar alongside the dashboard cards — so /docker/containers, /kubernetes/pods, /truenas/storage, /truenas/apps, /vmware/vms, and /vmware/storage shipped without search, status, grouping, or column controls. Operators had no way to filter inside a platform page short of navigating to the global Workloads/Storage page. Bridge those v5 affordances onto the v6 platform pages by extending the canonical surface contracts: - WorkloadsSurfaceProps gains `showFilterToolbar` and `suppressPlatformFilter`. `showFilterToolbar` keeps the canonical WorkloadsFilter (search input + status chips + view-mode segmented control + grouping toggle + ColumnPicker + sort handles) visible even under `tableOnly`. `suppressPlatformFilter` drops the redundant Platform chip since the platform is already fixed by the owning page, so the user never sees a removable lock. - StorageProps gains `showFilterToolbar`. Same idea for the canonical StoragePageControls (search + status + group-by + sort + node filter + view). Platform pages now mount their embedded surfaces with `tableOnly + showFilterToolbar` (plus `suppressPlatformFilter` for WorkloadsSurface): - Docker > Containers - Kubernetes > Pods - TrueNAS > Storage, Apps - vSphere > VMs, Storage UnifiedResourceTable-backed sub-tabs (Docker Hosts, K8s Clusters/ Nodes/Deployments, vSphere Hosts) still rely on the table's built-in sort handles only; a follow-up shared `PlatformInfraControls` row with search + counters is the next operator-controls bridge. Browser verification (Playwright, chromium, against live mock-mode Pulse dev runtime): - 9 tests, all pass. New assertion confirms that every embedded Workloads/Storage sub-tab on a platform page now renders the canonical search input (proving the v5-style operator toolbar is back). Targeted vitest: - WorkloadsSurface.performance.contract.test.tsx adds a platform-page embed contract assertion (37 tests total, all pass). - Storage.test.tsx adds the matching StorageProps assertion (39 tests total, all pass). Contracts updated: - performance-and-scalability.md Shared Boundaries: documents the `showFilterToolbar` + `suppressPlatformFilter` platform-page contract on WorkloadsSurface. - storage-recovery.md Shared Boundaries: documents the `showFilterToolbar` platform-page contract on StorageSurface. --- .../subsystems/performance-and-scalability.md | 12 +++++++ .../internal/subsystems/storage-recovery.md | 9 +++++ .../src/components/Storage/Storage.tsx | 10 +++++- .../Storage/__tests__/Storage.test.tsx | 8 +++++ .../components/Workloads/WorkloadsSurface.tsx | 2 +- ...loadsSurface.performance.contract.test.tsx | 11 ++++++ .../components/Workloads/useWorkloadsState.ts | 12 ++++++- .../src/features/docker/DockerPageSurface.tsx | 2 ++ .../kubernetes/KubernetesPageSurface.tsx | 2 ++ .../features/truenas/TrueNASPageSurface.tsx | 9 ++++- .../src/features/vmware/VmwarePageSurface.tsx | 9 ++++- .../tests/68-platform-pages-shell.spec.ts | 34 +++++++++++++++++++ 12 files changed, 115 insertions(+), 5 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 2be740142..f09443afa 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -541,6 +541,18 @@ shell clickable behind another overlay. cold dynamic import after the user clicks. New supported platform families must extend that registry rather than skipping the preload hot path; presentation-only platforms must not be registered. + Platform pages that embed `WorkloadsSurface` reuse the canonical + workloads filter toolbar through the `showFilterToolbar` + + `suppressPlatformFilter` props in `WorkloadsSurfaceProps`. The page + keeps `tableOnly` to hide the dashboard cards and summary strip but + opts in to the same shared `FilterBar`, `GroupedTableModeSegmentedControl`, + `ColumnPicker`, status/type chips, and search-history primitives that + the global Workloads page renders, so platform operators get + dense-table search, sort, grouping, view, status, and column controls + on every embedded workloads tab without spawning a forked toolbar. + The platform scope flows through `forcedPlatform` as a typed page + input; `suppressPlatformFilter` drops the now-redundant Platform chip + from the rendered toolbar so the user never sees a removable lock. ## Forbidden Paths diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index f9a52256b..8f8ab752c 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -850,6 +850,15 @@ bypass the API fail-closed execution gate. at the next-most-relevant canonical surface (for TrueNAS today that is the embedded `StorageSurface` at `/truenas/storage`) rather than at a placeholder Hosts overview that would render empty. + Platform pages that embed `StorageSurface` reuse the canonical + `StoragePageControls` toolbar through the `showFilterToolbar` prop on + `StorageProps`. The page keeps `tableOnly` to hide the storage summary + section but opts in to the shared search, status, group-by, sort, + node, view, and chart-collapse controls so platform operators get + dense-table storage controls on every embedded storage tab without + forking the toolbar. The source scope flows through + `forcedSourceFilter` as a typed page input; the source filter remains + available in the toolbar only when not forced. ## Forbidden Paths diff --git a/frontend-modern/src/components/Storage/Storage.tsx b/frontend-modern/src/components/Storage/Storage.tsx index 9bb2f78bd..94db2c6fe 100644 --- a/frontend-modern/src/components/Storage/Storage.tsx +++ b/frontend-modern/src/components/Storage/Storage.tsx @@ -15,6 +15,14 @@ type StorageProps = { tableOnly?: boolean; forcedView?: 'pools' | 'disks'; forcedSourceFilter?: string; + // Mirrors the WorkloadsSurface platform-page contract: when a platform + // page mounts StorageSurface inside its own chrome, set + // `showFilterToolbar` so the canonical StoragePageControls row stays + // visible alongside the table even when `tableOnly` hides the summary + // section. The page owns source scope via `forcedSourceFilter`; the + // controls toolbar still exposes search, status, grouping, sort, and + // node filters to the operator. + showFilterToolbar?: boolean; }; const Storage: Component = (props) => { @@ -167,7 +175,7 @@ const Storage: Component = (props) => {
- +
({ ), })); +describe('Storage platform-page embed contract', () => { + it('exposes showFilterToolbar on StorageProps so platform pages keep StoragePageControls visible under tableOnly', async () => { + const storageSource = (await import('../Storage.tsx?raw')).default; + expect(storageSource).toContain('showFilterToolbar?: boolean;'); + expect(storageSource).toContain('props.showFilterToolbar || !props.tableOnly'); + }); +}); + describe('Storage', () => { beforeEach(() => { vi.stubGlobal('fetch', fetchMock); diff --git a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx index 4abae278d..07e933435 100644 --- a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx @@ -82,7 +82,7 @@ export function WorkloadsSurface(props: WorkloadsSurfaceProps) {
{ await Promise.resolve(); }; +describe('Workloads platform-page embed contract', () => { + it('exposes showFilterToolbar and suppressPlatformFilter on WorkloadsSurfaceProps', async () => { + const stateSource = (await import('../useWorkloadsState.ts?raw')).default; + expect(stateSource).toContain('showFilterToolbar?: boolean;'); + expect(stateSource).toContain('suppressPlatformFilter?: boolean;'); + expect(stateSource).toContain('suppressPlatformFilter'); + const surfaceSource = (await import('../WorkloadsSurface.tsx?raw')).default; + expect(surfaceSource).toContain('props.showFilterToolbar || !props.tableOnly'); + }); +}); + describe('Workloads performance contract', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/frontend-modern/src/components/Workloads/useWorkloadsState.ts b/frontend-modern/src/components/Workloads/useWorkloadsState.ts index ce50a195f..876b88446 100644 --- a/frontend-modern/src/components/Workloads/useWorkloadsState.ts +++ b/frontend-modern/src/components/Workloads/useWorkloadsState.ts @@ -58,6 +58,14 @@ export interface WorkloadsSurfaceProps { tableOnly?: boolean; forcedPlatform?: string; forcedGroupingMode?: WorkloadsGroupingMode; + // When the surface is mounted inside a platform-first page, the page owns + // platform scope through `forcedPlatform`. Setting `showFilterToolbar` + // keeps the operator-facing WorkloadsFilter visible alongside the table + // even when `tableOnly` hides the summary cards/strip, and + // `suppressPlatformFilter` removes the redundant Platform chip from that + // filter row since the platform is already fixed by the owning page. + showFilterToolbar?: boolean; + suppressPlatformFilter?: boolean; } export type WorkloadSortKey = WorkloadsSortKey; @@ -406,7 +414,9 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) { navigate, nodeByInstance, namespaceFilterConfig, - platformFilterConfig, + platformFilterConfig: props.suppressPlatformFilter + ? () => undefined + : platformFilterConfig, platformOptions, reconnect, reconnectSurface, diff --git a/frontend-modern/src/features/docker/DockerPageSurface.tsx b/frontend-modern/src/features/docker/DockerPageSurface.tsx index 5023f6fb6..b0effa507 100644 --- a/frontend-modern/src/features/docker/DockerPageSurface.tsx +++ b/frontend-modern/src/features/docker/DockerPageSurface.tsx @@ -88,6 +88,8 @@ export function DockerPageSurface() { useWorkloads embedded tableOnly + showFilterToolbar + suppressPlatformFilter forcedPlatform={DOCKER_PLATFORM_FILTER} /> diff --git a/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx b/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx index b3b8e296f..089a5d065 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx @@ -102,6 +102,8 @@ export function KubernetesPageSurface() { useWorkloads embedded tableOnly + showFilterToolbar + suppressPlatformFilter forcedPlatform={KUBERNETES_PLATFORM_FILTER} /> diff --git a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx index 79692f057..ec0d37799 100644 --- a/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx +++ b/frontend-modern/src/features/truenas/TrueNASPageSurface.tsx @@ -74,7 +74,12 @@ export function TrueNASPageSurface() { } > - + diff --git a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx index 38934f644..b73d1e624 100644 --- a/frontend-modern/src/features/vmware/VmwarePageSurface.tsx +++ b/frontend-modern/src/features/vmware/VmwarePageSurface.tsx @@ -89,11 +89,18 @@ export function VmwarePageSurface() { useWorkloads embedded tableOnly + showFilterToolbar + suppressPlatformFilter forcedPlatform={VMWARE_PLATFORM_FILTER} /> - + diff --git a/tests/integration/tests/68-platform-pages-shell.spec.ts b/tests/integration/tests/68-platform-pages-shell.spec.ts index d40245b34..b45f40bf1 100644 --- a/tests/integration/tests/68-platform-pages-shell.spec.ts +++ b/tests/integration/tests/68-platform-pages-shell.spec.ts @@ -159,4 +159,38 @@ test.describe('Platform pages shell', () => { } }); } + + test('platform sub-tabs that embed Workloads/Storage surfaces expose v5-style operator controls', async ({ + page, + }, testInfo) => { + test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop chrome audit'); + + // Each entry verifies that the embedded canonical surface renders its + // operator toolbar (search + status/grouping/view chips) under + // platform-page chrome — not a stripped table-only view. + const cases: ReadonlyArray<{ path: string; testId: string }> = [ + { path: '/docker/containers', testId: 'docker-page' }, + { path: '/kubernetes/pods', testId: 'kubernetes-page' }, + { path: '/truenas/storage', testId: 'truenas-page' }, + { path: '/truenas/apps', testId: 'truenas-page' }, + { path: '/vmware/vms', testId: 'vmware-page' }, + { path: '/vmware/storage', testId: 'vmware-page' }, + ]; + + for (const c of cases) { + await page.goto(c.path, { waitUntil: 'domcontentloaded' }); + const pageRoot = page.getByTestId(c.testId); + await expect(pageRoot).toBeVisible({ timeout: 30_000 }); + + // The canonical Workloads/Storage operator toolbars use the shared + // FilterBar primitive, which renders a search input. If the platform + // page mistakenly stripped the toolbar, no search input would render + // inside the page region. + await expect( + pageRoot + .locator('input[type="search"], input[placeholder*="Search" i], input[placeholder*="filter" i]') + .first(), + ).toBeVisible({ timeout: 30_000 }); + } + }); });