mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
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:
parent
f81490ca4f
commit
65b069dbba
12 changed files with 115 additions and 5 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() &&
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ export function DockerPageSurface() {
|
|||
useWorkloads
|
||||
embedded
|
||||
tableOnly
|
||||
showFilterToolbar
|
||||
suppressPlatformFilter
|
||||
forcedPlatform={DOCKER_PLATFORM_FILTER}
|
||||
/>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ export function KubernetesPageSurface() {
|
|||
useWorkloads
|
||||
embedded
|
||||
tableOnly
|
||||
showFilterToolbar
|
||||
suppressPlatformFilter
|
||||
forcedPlatform={KUBERNETES_PLATFORM_FILTER}
|
||||
/>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue