mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Correct stale platform-page embed contract prose
Several v6 contracts still described platform pages embedding canonical surfaces through controls that no longer exist: a `tableOnly` / `embedded tableOnly` mode, a `showFilterToolbar` prop, a `RecoverySurface`, an embedded `StorageSurface` at /truenas/storage, and a TrueNAS Protection tab built on a Recovery surface. The aggregate Recovery surface was deleted on 2026-05-26 and neither WorkloadsSurface nor Storage has an embed mode. The platform-page rule in frontend-primitives.md (and its cloud-paid counterpart) now says what the pages do: the Proxmox and vSphere Overview tabs embed WorkloadsSurface with the page owning the workloads state and the one WorkloadsFilter toolbar, the Proxmox Storage tab embeds StorageSurface with forcedSourceFilter, and every other tab, including /truenas/storage, Proxmox Backups and TrueNAS Protection, renders a platform-owned table on the shared platform-page primitives. The old rule said pages only embed canonical surfaces and must not add per-family tables; that has not been true since the platform-first migration, so the rule now names the three embeds, keeps those tabs' rows on them, and keeps the bans on estate summary panels and forked table primitives. cloud-paid defers to that rule instead of carrying a second copy. The new guardrail case pins those embeds per page, the absence of an embed-mode prop on both surfaces, the platform lock on the Workloads embeds, and the recovery tabs' platform-owned tables, so the contract and the code cannot drift apart silently again.
This commit is contained in:
@@ -1161,18 +1161,21 @@ hands-on Patrol modes, issue investigation, verified fixes, and longer history`.
|
||||
absent platform families must not render as disabled placeholders in the
|
||||
paid or hosted app shell. Infrastructure is not an equal primary tab in
|
||||
that list. Workloads, Storage, and Recovery are not standalone aggregate
|
||||
workspace tabs; their component surfaces remain reusable inside platform
|
||||
pages via embedded `tableOnly` surfaces. Platform primary-tab settings
|
||||
workspace tabs. Workload and storage rows render inside platform pages,
|
||||
through the canonical `WorkloadsSurface` or `StorageSurface` where a page
|
||||
embeds one and through platform-owned tables elsewhere; recovery rows
|
||||
render only in platform-owned tabs such as Proxmox Backups and TrueNAS
|
||||
Protection, because no Recovery surface exists. Platform primary-tab settings
|
||||
handoffs in `AppLayout.tsx` must target the canonical
|
||||
`/settings/infrastructure` workspace and must not retain retired settings
|
||||
aliases such as `/settings/workloads/docker` or nested
|
||||
`/settings/infrastructure/platforms/*` paths. Each platform page must
|
||||
remain chrome-only: routing plus sub-tab navigation that embeds the
|
||||
canonical `WorkloadsSurface`, `StorageSurface`, or `RecoverySurface` in
|
||||
`embedded tableOnly` mode with a forced platform/source filter. The shell must not introduce dashboard cards,
|
||||
bespoke per-family tables, synthetic placeholder data, or reintroduce
|
||||
Infrastructure as a primary navigation entry without a governed contract
|
||||
decision recorded by the frontend-primitives owner.
|
||||
`/settings/infrastructure/platforms/*` paths. Which platform tabs embed a
|
||||
canonical surface and which render platform-owned tables is the
|
||||
frontend-primitives platform-page rule; cloud-paid must not carry a
|
||||
second copy of it. The paid or hosted shell must not introduce dashboard
|
||||
cards, synthetic placeholder data, or forked per-family table primitives,
|
||||
or reintroduce Infrastructure as a primary navigation entry, without a
|
||||
governed contract decision recorded by the frontend-primitives owner.
|
||||
The presence of the user-facing `Machines` tab, backed by the internal
|
||||
`standalone` route/id, is not a new commercial usage unit: hosted and paid
|
||||
surfaces continue to meter the governed monitored-system grouping result
|
||||
|
||||
@@ -3376,15 +3376,30 @@ Agent`), with the plain-language source phrase available through accessible
|
||||
default` instead of fusing provider and badge text such as
|
||||
`OpenRouterdefault`.
|
||||
Platform-first top-level pages registered through
|
||||
`frontend-modern/src/App.tsx` must stay chrome-only and route through the
|
||||
canonical app shell: each per-platform surface owns navigation and sub-tab
|
||||
chrome, then embeds the canonical `WorkloadsSurface`, `StorageSurface`, or
|
||||
`RecoverySurface` in `embedded tableOnly` mode with a forced platform or
|
||||
source filter. Per-platform features must not
|
||||
fork their own table primitives, header layouts, or summary cards when a
|
||||
shared canonical surface already exists; new shared platform-page
|
||||
primitives live under `frontend-modern/src/features/platformPage/` so the
|
||||
chrome stays reusable across families.
|
||||
`frontend-modern/src/App.tsx` route through the canonical app shell: each
|
||||
per-platform page surface owns its route, `PlatformSectionTabs` sub-tab
|
||||
navigation, and shared loading, empty, and error states, and its tabs are
|
||||
built around tables rather than estate overview or attention summary
|
||||
panels. Three tabs embed a cross-platform canonical surface with the
|
||||
platform scope locked: the Proxmox and vSphere Overview tabs embed
|
||||
`WorkloadsSurface` with `forcedPlatform` (the page owns the workloads
|
||||
state and the one `WorkloadsFilter` toolbar), and the Proxmox Storage tab
|
||||
embeds `StorageSurface` with `forcedSourceFilter`. Those tabs must keep
|
||||
their guests, VMs, and storage rows on those embeds rather than a
|
||||
platform-local copy. Neither surface has an `embedded` or `tableOnly`
|
||||
mode, and no `RecoverySurface` exists: the aggregate Recovery surface was
|
||||
deleted on 2026-05-26. Every other tab, including Docker containers,
|
||||
Kubernetes pods, Machines, `/truenas/storage`
|
||||
(`TrueNASStorageTopologyTable`), the Proxmox Backups tab, and the TrueNAS
|
||||
Protection tab, renders a platform-owned table built from the shared
|
||||
platform-page primitives in
|
||||
`frontend-modern/src/features/platformPage/sharedPlatformPage.tsx`, even
|
||||
where a canonical surface could list the same resource types.
|
||||
Per-platform features must not fork their own table primitives, header
|
||||
layouts, or summary cards when a shared canonical surface already exists;
|
||||
new shared platform-page primitives live under
|
||||
`frontend-modern/src/features/platformPage/` so the chrome stays reusable
|
||||
across families.
|
||||
Source-specific platform product surfaces under
|
||||
`frontend-modern/src/features/`, such as the Proxmox Backups tab, may own
|
||||
domain IA and row models in their product subsystem while consuming shared
|
||||
@@ -4420,12 +4435,13 @@ labels) bounded — individual subject names and full PolicyRule contents stay
|
||||
outside the rendered surface, mirroring the agent and unified-resource
|
||||
contracts.
|
||||
|
||||
Embedded Recovery workspace controls now use the shared filter-toolbar
|
||||
primitive boundary. Platform pages may choose a default Recovery workspace,
|
||||
such as TrueNAS opening on protection coverage, but the compact
|
||||
protection/events selector must use `FilterSegmentedControl` and the
|
||||
recovery-owned `useRecoverySurfaceState` owner rather than page-local tabs,
|
||||
nested cards, or independent protection/event state in the embedding surface.
|
||||
No platform page embeds a Recovery workspace. The aggregate Recovery surface,
|
||||
its protection/events selector, and `useRecoverySurfaceState` were deleted on
|
||||
2026-05-26, and TrueNAS opens on its Overview tab. Recovery rows render in
|
||||
platform-owned tabs whose controls stay on the shared filter-toolbar
|
||||
primitives: the Proxmox Backups tab filters through the shared `FilterBar`,
|
||||
and `TrueNASProtectionTable.tsx` through `PlatformTableToolbar`, rather than
|
||||
page-local tab strips or nested cards.
|
||||
|
||||
Cross-jump chip strips on alert and Patrol surfaces were retired on
|
||||
2026-05-16 alongside the platform-first migration. The
|
||||
|
||||
@@ -1691,18 +1691,26 @@ shell clickable behind another overlay.
|
||||
scoped table metric-history interaction that requested them. Platform
|
||||
landing should stay route-module warm and data-light until the selected
|
||||
platform page owns its normal resource/table query.
|
||||
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.
|
||||
Two platform pages embed `WorkloadsSurface`: the Proxmox and vSphere
|
||||
Overview tabs (`frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx`
|
||||
and `frontend-modern/src/features/vmware/VmwarePageSurface.tsx`). Each page
|
||||
builds the workloads state itself with `useWorkloadsState`, renders the
|
||||
one shared `WorkloadsFilter` toolbar (search, status filter, a type
|
||||
filter that vSphere suppresses, and the View options for grouping, metric display, and columns on the
|
||||
shared `FilterBar`) from that state, and hands the same state to `WorkloadsSurface` through its
|
||||
`state` prop with `suppressFilterToolbar`, so the surface skips its own
|
||||
filter row and the page never stacks two toolbars wired to one state.
|
||||
Proxmox also passes that state's search, plus its page-owned metric mode
|
||||
and history range, to the nodes table above the guests, so one toolbar
|
||||
drives both tables. `WorkloadsSurface` has no `tableOnly` mode, no
|
||||
`showFilterToolbar` prop, and no dashboard cards or summary strip to
|
||||
hide: it renders its filter row (unless suppressed or in kiosk mode), the
|
||||
workloads table, or an empty-state table card. The platform scope flows
|
||||
through `forcedPlatform` as a typed page input to `useWorkloadsState`,
|
||||
the toolbar, and the surface. `suppressPlatformFilter: true` makes the
|
||||
state expose no Platform filter config and both pages pass
|
||||
`platformFilter={undefined}` to the toolbar, so the user never sees the
|
||||
locked platform as a removable chip.
|
||||
|
||||
### Navigation resolves from a bounded request
|
||||
|
||||
|
||||
@@ -3483,9 +3483,10 @@ the platform-first migration. The chip strips that previously rendered them
|
||||
inside the alert resource-incidents panel and Patrol findings panel were
|
||||
retired in the same pass; storage and recovery drilldowns now stay inside
|
||||
the platform-page sub-tabs rather than offering external surface jumps.
|
||||
Future cross-surface storage or recovery affordances must compose against
|
||||
the embedded `StorageSurface` / `RecoverySurface` consumers rather than
|
||||
reintroducing top-level URL builders.
|
||||
Future cross-surface storage or recovery affordances must link into the
|
||||
platform tab that owns those rows (the Proxmox Storage tab's embedded
|
||||
`StorageSurface`, `/truenas/storage`, the Proxmox Backups tab, or the TrueNAS
|
||||
Protection tab) rather than reintroducing top-level URL builders.
|
||||
The remaining storage and recovery route builders in
|
||||
`frontend-modern/src/routing/resourceLinks.ts` are query-state serializers, not
|
||||
destination builders: callers must append `buildStorageRouteSearch()` or
|
||||
@@ -3498,21 +3499,24 @@ storage source, node, resource, workspace, rollup, or day state. Adding a
|
||||
shareable filter query to another surface must not broaden or reinterpret the
|
||||
storage and recovery serializer contract.
|
||||
|
||||
Storage and Recovery can now be embedded by a platform page in table-only mode
|
||||
with a forced platform source/filter. Proxmox uses that embedding for
|
||||
source-scoped storage and recovery history, while the TrueNAS Protection tab
|
||||
uses the same Recovery surface with a forced `truenas` platform filter and the
|
||||
protection-coverage workspace as its default entry point. The embedded mode
|
||||
suppresses standalone page chrome, summary charts, and full filter chrome, but
|
||||
the Storage surface must keep the canonical Storage / Physical Disks view
|
||||
selector inside the table header unless the embedding explicitly locks a
|
||||
`forcedView`, and the Recovery surface must keep protection/events workspace
|
||||
state in `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts`.
|
||||
The canonical route-backed filter state, fetch builders, table rendering, and
|
||||
storage/recovery vocabulary remain owned by the Storage and Recovery surfaces.
|
||||
Platform pages must compose those owners rather than cloning storage pools,
|
||||
physical disks, recovery events, or protected-inventory tables under
|
||||
platform-specific data contracts.
|
||||
A platform page embeds `StorageSurface` with a forced source filter; there is
|
||||
no table-only mode, and no Recovery surface exists to embed. The Proxmox
|
||||
Storage tab is the only embedder: it passes `forcedSourceFilter`, and the
|
||||
surface keeps the canonical Storage / Physical Disks view selector because
|
||||
Proxmox locks no `forcedView`. Recovery rows render in platform-owned tabs.
|
||||
The Proxmox Backups tab (`frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx`)
|
||||
reads `/api/backups/pve`, `/api/backups/pbs`, and protection postures itself,
|
||||
and the TrueNAS Protection tab feeds
|
||||
`frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx` from
|
||||
`useRecoveryPoints` with the `truenas` platform forced. `/truenas/storage`
|
||||
renders `TrueNASStorageTopologyTable`, not `StorageSurface`. The Storage
|
||||
surface keeps owning its route-backed filter state, fetch builders, table
|
||||
rendering, and storage vocabulary for its embedder. The Proxmox Storage tab
|
||||
must keep its storage pools and physical disks on that embed rather than a
|
||||
Proxmox-local copy; TrueNAS storage is deliberately a TrueNAS-owned topology
|
||||
table built from the TrueNAS page model. Platform recovery tables must read
|
||||
the canonical backup, posture, and recovery-point transports rather than a
|
||||
platform-specific recovery data contract.
|
||||
|
||||
The investigation enrichment path reads operator-state from the
|
||||
in-memory provider already wired against the durable
|
||||
@@ -5996,16 +6000,19 @@ settings, but they must not reinterpret provider auth, provider connection, or
|
||||
model-selection causes as recovery-source health, backup readiness, or
|
||||
storage-control capability.
|
||||
That bounded projection is the current TrueNAS floor for storage and recovery:
|
||||
operators can inspect TrueNAS pools, datasets, disks, snapshots, and
|
||||
replication artifacts through the shared storage and recovery pages plus
|
||||
cross-surface handoffs. Storage and recovery do not promise a TrueNAS-local
|
||||
onboarding path, restore/control plane, or separate diagnostic transport;
|
||||
backend-native app actions, logs, and config reads stay on the adjacent
|
||||
AI/runtime path and only feed refreshed canonical recovery/state afterward.
|
||||
The TrueNAS platform page may embed that same canonical recovery surface as a
|
||||
scoped Protection tab, but it must keep the `truenas` platform filter forced,
|
||||
reuse recovery-owned protection/event workspace state, and avoid growing a
|
||||
TrueNAS-only snapshot or replication table contract.
|
||||
operators can inspect TrueNAS pools, datasets, and disks on the TrueNAS
|
||||
page's Storage tab (`TrueNASStorageTopologyTable`) and snapshots and
|
||||
replication artifacts on its Protection tab. Storage and recovery do not
|
||||
promise a TrueNAS-local onboarding path, restore/control plane, or separate
|
||||
diagnostic transport; backend-native app actions, logs, and config reads stay
|
||||
on the adjacent AI/runtime path and only feed refreshed canonical
|
||||
recovery/state afterward.
|
||||
The Protection tab is a platform-owned table, not an embedded recovery
|
||||
surface: `TrueNASProtectionTable.tsx` lists the recovery points
|
||||
`useRecoveryPoints` returns with the `truenas` platform filter forced. It must
|
||||
keep that filter forced and keep reading the canonical recovery-points
|
||||
transport rather than growing a TrueNAS-only snapshot or replication data
|
||||
contract.
|
||||
VMware vSphere is the current admitted narrower phase-1 direction. Storage and
|
||||
recovery may consume vCenter-backed
|
||||
datastore inventory plus VM snapshot-tree visibility as shared storage and
|
||||
|
||||
@@ -2489,8 +2489,10 @@ signal source instead of advertising empty object browsers. The
|
||||
canonical TrueNAS adapter (`internal/truenas/provider.go::
|
||||
truenasRecordsFromSnapshot`) already emits the top-level TrueNAS
|
||||
appliance as a unified `agent` row tagged with the `truenas`
|
||||
platform, so TrueNAS defaults to `/truenas/overview` (the Systems
|
||||
sub-tab); the embedded `StorageSurface` lives at `/truenas/storage`.
|
||||
platform, so TrueNAS defaults to `/truenas/overview`, whose Overview
|
||||
sub-tab lists those systems in `TrueNASSystemsTable`. `/truenas/storage`
|
||||
renders the TrueNAS-owned `TrueNASStorageTopologyTable`, not
|
||||
`StorageSurface`; only the Proxmox Storage tab embeds `StorageSurface`.
|
||||
Any future platform that wants to default to a Systems / Hosts
|
||||
overview must first have its canonical resource adapter project the
|
||||
platform's top-level system as a unified resource so the builder
|
||||
|
||||
+192
-1
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import * as ts from 'typescript';
|
||||
import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
|
||||
import agentMachineTableModelSource from '@/features/standalone/agentMachineTableModel.ts?raw';
|
||||
import standalonePageModelSource from '@/features/standalone/standalonePageModel.ts?raw';
|
||||
@@ -62,6 +63,9 @@ import vsphereAlertsTableSource from '@/features/vmware/VsphereAlertsTable.tsx?r
|
||||
import vsphereDatastoresTableSource from '@/features/vmware/VsphereDatastoresTable.tsx?raw';
|
||||
import vsphereHostsTableSource from '@/features/vmware/VsphereHostsTable.tsx?raw';
|
||||
import vsphereNetworksTableSource from '@/features/vmware/VsphereNetworksTable.tsx?raw';
|
||||
import storageSurfaceSource from '@/components/Storage/Storage.tsx?raw';
|
||||
import useWorkloadsStateSource from '@/components/Workloads/useWorkloadsState.ts?raw';
|
||||
import workloadsSurfaceSource from '@/components/Workloads/WorkloadsSurface.tsx?raw';
|
||||
|
||||
const indexCssSource = readFileSync('src/index.css', 'utf8');
|
||||
|
||||
@@ -175,6 +179,81 @@ const proxmoxInlineDetailTableSources = [
|
||||
proxmoxMailGatewayDrawerSource,
|
||||
];
|
||||
|
||||
type JsxTag = ts.JsxOpeningElement | ts.JsxSelfClosingElement;
|
||||
|
||||
function parseTsx(source: string): ts.SourceFile {
|
||||
return ts.createSourceFile('source.tsx', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
}
|
||||
|
||||
/** Every node under `root` (inclusive) that passes `test`, in source order. */
|
||||
function findNodes<T extends ts.Node>(root: ts.Node, test: (node: ts.Node) => node is T): T[] {
|
||||
const found: T[] = [];
|
||||
const visit = (node: ts.Node) => {
|
||||
if (test(node)) found.push(node);
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(root);
|
||||
return found;
|
||||
}
|
||||
|
||||
function isJsxTag(node: ts.Node): node is JsxTag {
|
||||
return ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node);
|
||||
}
|
||||
|
||||
function jsxTags(root: ts.Node, tag: string): JsxTag[] {
|
||||
return findNodes(root, isJsxTag).filter((node) => node.tagName.getText() === tag);
|
||||
}
|
||||
|
||||
/** The single `<tag>` under `root`; throws unless there is exactly one. */
|
||||
function onlyJsxTag(root: ts.Node, tag: string): JsxTag {
|
||||
const tags = jsxTags(root, tag);
|
||||
const [only] = tags;
|
||||
if (tags.length !== 1 || !only) throw new Error(`expected one <${tag}>, found ${tags.length}`);
|
||||
return only;
|
||||
}
|
||||
|
||||
/** Source text of an attribute's value; a bare boolean attribute reads as `true`. */
|
||||
function jsxAttr(tag: JsxTag, name: string): string | undefined {
|
||||
const attr = tag.attributes.properties.find(
|
||||
(prop): prop is ts.JsxAttribute => ts.isJsxAttribute(prop) && prop.name.getText() === name,
|
||||
);
|
||||
if (!attr) return undefined;
|
||||
if (!attr.initializer) return 'true';
|
||||
return ts.isJsxExpression(attr.initializer)
|
||||
? attr.initializer.expression?.getText()
|
||||
: attr.initializer.getText();
|
||||
}
|
||||
|
||||
function functionDecl(root: ts.Node, name: string): ts.FunctionDeclaration {
|
||||
const fn = findNodes(root, ts.isFunctionDeclaration).find((node) => node.name?.text === name);
|
||||
if (!fn) throw new Error(`function ${name} not found`);
|
||||
return fn;
|
||||
}
|
||||
|
||||
/** Initializer text of every object property inside the first `callee(...)` call. */
|
||||
function callProperties(root: ts.Node, callee: string): Map<string, string> {
|
||||
const call = findNodes(root, ts.isCallExpression).find(
|
||||
(node) => node.expression.getText() === callee,
|
||||
);
|
||||
const properties = new Map<string, string>();
|
||||
for (const property of call ? findNodes(call, ts.isPropertyAssignment) : []) {
|
||||
properties.set(property.name.getText(), property.initializer.getText());
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
/** Tab id of the nearest enclosing `activeTab() === 'x'` / `mountedTabs().has('x')` gate. */
|
||||
function enclosingTab(tag: ts.Node): string | undefined {
|
||||
for (let node = tag.parent; node; node = node.parent) {
|
||||
if (!ts.isJsxElement(node) || node.openingElement.tagName.getText() !== 'Show') continue;
|
||||
const gate = (jsxAttr(node.openingElement, 'when') ?? '').match(
|
||||
/^activeTab\(\) === '([a-z-]+)'$|^mountedTabs\(\)\.has\('([a-z-]+)'\)$/,
|
||||
);
|
||||
if (gate) return gate[1] ?? gate[2];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
describe('platform overview layout guardrails', () => {
|
||||
it('keeps attention summaries canonical without a competing estate panel', () => {
|
||||
for (const source of [truenasProtectionTableSource, vsphereAlertsTableSource]) {
|
||||
@@ -697,6 +776,118 @@ describe('platform overview layout guardrails', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps canonical surface embeds to the tabs the platform-page contract names', () => {
|
||||
// The platform-page rule in frontend-primitives.md (mirrored by
|
||||
// cloud-paid, storage-recovery, unified-resources and
|
||||
// performance-and-scalability) names exactly these embeds:
|
||||
// WorkloadsSurface on the Proxmox and vSphere Overview tabs,
|
||||
// StorageSurface on the Proxmox Storage tab, and no Recovery surface.
|
||||
const proxmoxFile = parseTsx(proxmoxPageSurfaceSource);
|
||||
const truenasFile = parseTsx(truenasPageSurfaceSource);
|
||||
const expectedEmbeds: Array<[string, ts.SourceFile, { workloads: number; storage: number }]> = [
|
||||
[
|
||||
'StandalonePageSurface',
|
||||
parseTsx(standalonePageSurfaceSource),
|
||||
{ workloads: 0, storage: 0 },
|
||||
],
|
||||
['ProxmoxPageSurface', proxmoxFile, { workloads: 1, storage: 1 }],
|
||||
['DockerPageSurface', parseTsx(dockerPageSurfaceSource), { workloads: 0, storage: 0 }],
|
||||
[
|
||||
'KubernetesPageSurface',
|
||||
parseTsx(kubernetesPageSurfaceSource),
|
||||
{ workloads: 0, storage: 0 },
|
||||
],
|
||||
['TrueNASPageSurface', truenasFile, { workloads: 0, storage: 0 }],
|
||||
['VmwarePageSurface', parseTsx(vmwarePageSurfaceSource), { workloads: 1, storage: 0 }],
|
||||
];
|
||||
for (const [name, file, expected] of expectedEmbeds) {
|
||||
expect(jsxTags(file, 'WorkloadsSurface').length, `${name} WorkloadsSurface embeds`).toBe(
|
||||
expected.workloads,
|
||||
);
|
||||
expect(jsxTags(file, 'StorageSurface').length, `${name} StorageSurface embeds`).toBe(
|
||||
expected.storage,
|
||||
);
|
||||
const recoveryEmbeds = findNodes(file, isJsxTag)
|
||||
.map((tag) => tag.tagName.getText())
|
||||
.filter((tag) => /^Recovery(?:Surface)?$/.test(tag));
|
||||
expect(recoveryEmbeds, `${name} must not embed a Recovery surface`).toEqual([]);
|
||||
}
|
||||
expect(existsSync('src/features/recovery')).toBe(false);
|
||||
expect(existsSync('src/components/Recovery')).toBe(false);
|
||||
|
||||
// Neither surface has an embedded or table-only mode, and WorkloadsSurface
|
||||
// has no opt-in toolbar prop: the embedding page suppresses its toolbar.
|
||||
const surfaceSources: Array<[string, string]> = [
|
||||
['WorkloadsSurface', workloadsSurfaceSource],
|
||||
['useWorkloadsState', useWorkloadsStateSource],
|
||||
['Storage', storageSurfaceSource],
|
||||
];
|
||||
for (const [name, source] of surfaceSources) {
|
||||
expect(source, `${name} must not declare an embed-mode prop`).not.toMatch(
|
||||
/\b(?:embedded|tableOnly|showFilterToolbar)\??:/,
|
||||
);
|
||||
expect(source, `${name} must not read an embed-mode prop`).not.toMatch(
|
||||
/props\.(?:embedded|tableOnly|showFilterToolbar)\b/,
|
||||
);
|
||||
}
|
||||
|
||||
// Each Workloads embed sits on its page's Overview tab, owns the state and
|
||||
// the one toolbar, and locks the platform: the state is scoped to it and
|
||||
// the Platform chip never shows as a removable filter.
|
||||
const workloadsEmbedders: Array<[string, ts.SourceFile, string, string]> = [
|
||||
['ProxmoxPageSurface', proxmoxFile, 'ProxmoxOverview', 'PROXMOX_PLATFORM_FILTER'],
|
||||
[
|
||||
'VmwarePageSurface',
|
||||
parseTsx(vmwarePageSurfaceSource),
|
||||
'VmwareOverview',
|
||||
'VMWARE_PLATFORM_FILTER',
|
||||
],
|
||||
];
|
||||
for (const [name, file, overview, platform] of workloadsEmbedders) {
|
||||
expect(enclosingTab(onlyJsxTag(file, overview)), `${name} overview tab`).toBe('overview');
|
||||
const overviewFn = functionDecl(file, overview);
|
||||
const state = callProperties(overviewFn, 'useWorkloadsState');
|
||||
expect(state.get('forcedPlatform'), name).toBe(platform);
|
||||
expect(state.get('suppressPlatformFilter'), name).toBe('true');
|
||||
const toolbar = onlyJsxTag(overviewFn, 'WorkloadsFilter');
|
||||
expect(jsxAttr(toolbar, 'platformFilter'), name).toBe('undefined');
|
||||
expect(jsxAttr(toolbar, 'forcedPlatform'), name).toBe(platform);
|
||||
const surface = onlyJsxTag(overviewFn, 'WorkloadsSurface');
|
||||
expect(jsxAttr(surface, 'state'), name).toBe('workloadsState');
|
||||
expect(jsxAttr(surface, 'suppressFilterToolbar'), name).toBe('true');
|
||||
}
|
||||
|
||||
// StorageSurface sits on the Proxmox Storage tab with the source locked and
|
||||
// no forcedView, so Storage keeps its Storage / Physical Disks view tabs.
|
||||
const storageEmbed = onlyJsxTag(proxmoxFile, 'StorageSurface');
|
||||
expect(enclosingTab(storageEmbed)).toBe('storage');
|
||||
expect(jsxAttr(storageEmbed, 'forcedSourceFilter')).toBe('PROXMOX_PLATFORM_FILTER');
|
||||
expect(jsxAttr(storageEmbed, 'forcedView')).toBeUndefined();
|
||||
const storageControls = onlyJsxTag(parseTsx(storageSurfaceSource), 'StoragePageControls');
|
||||
expect(jsxAttr(storageControls, 'showViewTabs')).toBe('!props.forcedView');
|
||||
|
||||
// TrueNAS storage and recovery rows render in TrueNAS-owned tables, and
|
||||
// the Proxmox Backups tab is a Proxmox-owned table.
|
||||
expect(enclosingTab(onlyJsxTag(truenasFile, 'TrueNASStorage'))).toBe('storage');
|
||||
onlyJsxTag(functionDecl(truenasFile, 'TrueNASStorage'), 'TrueNASStorageTopologyTable');
|
||||
expect(enclosingTab(onlyJsxTag(proxmoxFile, 'ProxmoxBackupsTable'))).toBe('backups');
|
||||
expect(callProperties(truenasFile, 'useRecoveryPoints').get('platform')).toBe(
|
||||
'TRUENAS_PLATFORM_FILTER',
|
||||
);
|
||||
const protectionHook = findNodes(truenasFile, ts.isVariableDeclaration).find(
|
||||
(declaration) => declaration.name.getText() === 'protection',
|
||||
);
|
||||
expect(protectionHook?.initializer?.getText()).toMatch(/^useRecoveryPoints\(/);
|
||||
const protectionTab = onlyJsxTag(truenasFile, 'TrueNASProtection');
|
||||
expect(enclosingTab(protectionTab)).toBe('protection');
|
||||
expect(jsxAttr(protectionTab, 'recoveryPoints')).toBe('protection');
|
||||
const protectionTable = onlyJsxTag(
|
||||
functionDecl(truenasFile, 'TrueNASProtection'),
|
||||
'TrueNASProtectionTable',
|
||||
);
|
||||
expect(jsxAttr(protectionTable, 'points')).toBe('props.recoveryPoints.points()');
|
||||
});
|
||||
|
||||
it('keeps mobile host tables focused on useful operational columns', () => {
|
||||
// Assertions use the canonical kind-based helpers — either
|
||||
// getPlatformTableHeadClassForKind('<kind>') on a raw TableHead or the
|
||||
|
||||
Reference in New Issue
Block a user