frontend(platforms): restore v5-style operator controls on embedded canonical surfaces

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.
This commit is contained in:
rcourtman 2026-05-16 00:02:16 +01:00
parent f81490ca4f
commit 65b069dbba
12 changed files with 115 additions and 5 deletions

View file

@ -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

View file

@ -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

View file

@ -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<StorageProps> = (props) => {
@ -167,7 +175,7 @@ const Storage: Component<StorageProps> = (props) => {
</Show>
<div class="space-y-4" data-testid="storage-interaction-surface">
<Show when={!props.tableOnly}>
<Show when={props.showFilterToolbar || !props.tableOnly}>
<div data-summary-clear-ignore>
<StoragePageControls
kioskMode={kioskMode}

View file

@ -347,6 +347,14 @@ vi.mock('@/components/Storage/DiskList', () => ({
),
}));
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);

View file

@ -82,7 +82,7 @@ export function WorkloadsSurface(props: WorkloadsSurfaceProps) {
<div class="space-y-3" data-testid="workloads-interaction-surface">
<Show
when={
!props.tableOnly &&
(props.showFilterToolbar || !props.tableOnly) &&
!state.kioskMode() &&
state.surfaceConnected() &&
state.surfaceInitialDataReceived() &&

View file

@ -354,6 +354,17 @@ const flushEffects = async () => {
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();

View file

@ -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,

View file

@ -88,6 +88,8 @@ export function DockerPageSurface() {
useWorkloads
embedded
tableOnly
showFilterToolbar
suppressPlatformFilter
forcedPlatform={DOCKER_PLATFORM_FILTER}
/>
</Show>

View file

@ -102,6 +102,8 @@ export function KubernetesPageSurface() {
useWorkloads
embedded
tableOnly
showFilterToolbar
suppressPlatformFilter
forcedPlatform={KUBERNETES_PLATFORM_FILTER}
/>
</Show>

View file

@ -74,7 +74,12 @@ export function TrueNASPageSurface() {
}
>
<Show when={activeTab() === 'storage'}>
<StorageSurface embedded tableOnly forcedSourceFilter={TRUENAS_PLATFORM_FILTER} />
<StorageSurface
embedded
tableOnly
showFilterToolbar
forcedSourceFilter={TRUENAS_PLATFORM_FILTER}
/>
</Show>
<Show when={activeTab() === 'apps'}>
<WorkloadsSurface
@ -84,6 +89,8 @@ export function TrueNASPageSurface() {
useWorkloads
embedded
tableOnly
showFilterToolbar
suppressPlatformFilter
forcedPlatform={TRUENAS_PLATFORM_FILTER}
/>
</Show>

View file

@ -89,11 +89,18 @@ export function VmwarePageSurface() {
useWorkloads
embedded
tableOnly
showFilterToolbar
suppressPlatformFilter
forcedPlatform={VMWARE_PLATFORM_FILTER}
/>
</Show>
<Show when={activeTab() === 'storage'}>
<StorageSurface embedded tableOnly forcedSourceFilter={VMWARE_PLATFORM_FILTER} />
<StorageSurface
embedded
tableOnly
showFilterToolbar
forcedSourceFilter={VMWARE_PLATFORM_FILTER}
/>
</Show>
</Show>
</Show>

View file

@ -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 });
}
});
});