mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Canonicalize platform error states
This commit is contained in:
parent
0109361296
commit
51ac42870c
8 changed files with 179 additions and 28 deletions
|
|
@ -2398,6 +2398,12 @@ Platform table loading states are registry-backed too.
|
|||
`role="status"` loading row for platform pages and tables; platform consumers
|
||||
own only the title and description copy. Platform feature surfaces must not
|
||||
recreate the `TableCard` plus compact status-row shell locally.
|
||||
Platform load-failure states are registry-backed as well.
|
||||
`PlatformErrorState` owns the repeated table-card error shell, warning icon,
|
||||
and Refresh action for platform page and table load failures; platform
|
||||
consumers own only the failure title, description, and refresh callback.
|
||||
Platform feature surfaces must not recreate an `EmptyState` plus local Refresh
|
||||
button for `Could not load...` states.
|
||||
Filter bars are registry-backed too. `FilterBar` owns resource-list filtering
|
||||
as a catalog of `FilterDef` entries, while `filterChipStatusDot` owns the
|
||||
small leading status-dot glyph used by filter options. Page and feature
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@ interpreted as backup visibility, recovery readiness, restore capability, or a
|
|||
storage/recovery-owned command path. Suppressing those notices when the API has
|
||||
no deployable agent update target is likewise lifecycle/frontend behavior and
|
||||
does not change Proxmox backup or recovery evidence.
|
||||
Proxmox backup inventory loading and load-failure chrome is likewise a
|
||||
frontend-primitives dependency. `ProxmoxBackupsTable` owns the backup API
|
||||
queries, recovery model, filters, coverage split, and backup-specific error
|
||||
copy, but repeated loading and retry shells must compose
|
||||
`PlatformTableLoadingState` and `PlatformErrorState`; storage/recovery must not
|
||||
fork local table-card loading rows or refresh-button error empty states.
|
||||
Tenant report branding settings are adjacent tenant-local configuration, not a
|
||||
storage or recovery product state. `reportBranding` persisted in a tenant
|
||||
runtime's `system.json` should be preserved by the existing tenant data
|
||||
|
|
|
|||
|
|
@ -224,6 +224,13 @@ the source-specific empty-state vocabulary, action choice, and evidence rule
|
|||
that decides why a table is empty, but the table-card empty-state shell itself
|
||||
must compose the frontend-primitives-owned `PlatformTableEmptyState` primitive
|
||||
instead of importing `EmptyState` directly or wrapping it in a page-local card.
|
||||
Platform loading and load-failure states follow the same boundary.
|
||||
Unified-resource consumers such as `ProxmoxReplicationTable` own the API query,
|
||||
resource-specific copy, retry callback, and evidence rule that decides which
|
||||
state applies, but repeated loading rows and retry shells must compose the
|
||||
frontend-primitives-owned `PlatformTableLoadingState` and `PlatformErrorState`
|
||||
primitives instead of recreating page-local table-card status rows or local
|
||||
Refresh-button error empty states.
|
||||
The same split applies to inline detail row shells on platform, native-resource,
|
||||
workload, and infrastructure tables. Unified-resource consumers own row
|
||||
identity, source-specific drawer content, expansion state, and resource
|
||||
|
|
|
|||
|
|
@ -125,7 +125,34 @@
|
|||
"requiredConsumers": [
|
||||
{ "path": "src/features/docker/DockerPageSurface.tsx" },
|
||||
{ "path": "src/features/kubernetes/KubernetesPageSurface.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxPageSurface.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
||||
{ "path": "src/features/standalone/StandalonePageSurface.tsx" },
|
||||
{ "path": "src/features/truenas/TrueNASPageSurface.tsx" },
|
||||
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
||||
{ "path": "src/features/vmware/VmwarePageSurface.tsx" }
|
||||
],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-error-state",
|
||||
"category": "platform-error-state",
|
||||
"summary": "Platform page and table load failures must compose PlatformErrorState instead of recreating a local EmptyState plus Refresh action.",
|
||||
"canonical": {
|
||||
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"export": "PlatformErrorState"
|
||||
},
|
||||
"requiredConsumers": [
|
||||
{ "path": "src/features/docker/DockerPageSurface.tsx" },
|
||||
{ "path": "src/features/kubernetes/KubernetesPageSurface.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxPageSurface.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
||||
{ "path": "src/features/standalone/StandalonePageSurface.tsx" },
|
||||
{ "path": "src/features/truenas/TrueNASPageSurface.tsx" },
|
||||
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
||||
|
|
@ -384,6 +411,31 @@
|
|||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "platform-error-state-required",
|
||||
"category": "platform-error-state",
|
||||
"summary": "Platform feature files that surface load-failure copy must compose PlatformErrorState for the canonical retry shell.",
|
||||
"canonical": {
|
||||
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"export": "PlatformErrorState"
|
||||
},
|
||||
"scopes": [
|
||||
"src/features/docker",
|
||||
"src/features/kubernetes",
|
||||
"src/features/proxmox",
|
||||
"src/features/standalone",
|
||||
"src/features/truenas",
|
||||
"src/features/vmware"
|
||||
],
|
||||
"extensions": [".tsx"],
|
||||
"triggerPatterns": ["title=\"Could not load"],
|
||||
"requiredPatterns": ["PlatformErrorState"],
|
||||
"allowedPaths": [],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "named-detail-table-row-shared-shell",
|
||||
"category": "row-detail",
|
||||
|
|
|
|||
|
|
@ -1056,7 +1056,6 @@ describe('shared primitive guardrails', () => {
|
|||
for (const source of [
|
||||
dockerHostsTableSource,
|
||||
kubernetesNodesTableSource,
|
||||
proxmoxBackupsTableSource,
|
||||
proxmoxCephTableSource,
|
||||
proxmoxCoverageTableSource,
|
||||
proxmoxMailGatewayTableSource,
|
||||
|
|
@ -1096,7 +1095,9 @@ describe('shared primitive guardrails', () => {
|
|||
const requiredConsumerPaths = [
|
||||
'src/features/docker/DockerPageSurface.tsx',
|
||||
'src/features/kubernetes/KubernetesPageSurface.tsx',
|
||||
'src/features/proxmox/ProxmoxBackupsTable.tsx',
|
||||
'src/features/proxmox/ProxmoxPageSurface.tsx',
|
||||
'src/features/proxmox/ProxmoxReplicationTable.tsx',
|
||||
'src/features/standalone/StandalonePageSurface.tsx',
|
||||
'src/features/truenas/TrueNASPageSurface.tsx',
|
||||
'src/features/truenas/TrueNASProtectionTable.tsx',
|
||||
|
|
@ -1137,7 +1138,9 @@ describe('shared primitive guardrails', () => {
|
|||
for (const source of [
|
||||
dockerPageSurfaceSource,
|
||||
kubernetesPageSurfaceSource,
|
||||
proxmoxBackupsTableSource,
|
||||
proxmoxPageSurfaceSource,
|
||||
proxmoxReplicationTableSource,
|
||||
standalonePageSurfaceSource,
|
||||
truenasPageSurfaceSource,
|
||||
truenasProtectionTableSource,
|
||||
|
|
@ -1148,6 +1151,87 @@ describe('shared primitive guardrails', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('keeps platform load-failure states on the shared error template', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
rules?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
requiredConsumers?: Array<{ path?: string }>;
|
||||
}>;
|
||||
requiredPatternGuards?: Array<{
|
||||
id: string;
|
||||
canonical?: { path?: string; export?: string };
|
||||
triggerPatterns?: string[];
|
||||
requiredPatterns?: string[];
|
||||
scopes?: string[];
|
||||
allowedPaths?: string[];
|
||||
}>;
|
||||
};
|
||||
const registeredRule = registry.rules?.find((rule) => rule.id === 'platform-error-state');
|
||||
const requiredGuard = registry.requiredPatternGuards?.find(
|
||||
(guard) => guard.id === 'platform-error-state-required',
|
||||
);
|
||||
const requiredConsumerPaths = [
|
||||
'src/features/docker/DockerPageSurface.tsx',
|
||||
'src/features/kubernetes/KubernetesPageSurface.tsx',
|
||||
'src/features/proxmox/ProxmoxBackupsTable.tsx',
|
||||
'src/features/proxmox/ProxmoxPageSurface.tsx',
|
||||
'src/features/proxmox/ProxmoxReplicationTable.tsx',
|
||||
'src/features/standalone/StandalonePageSurface.tsx',
|
||||
'src/features/truenas/TrueNASPageSurface.tsx',
|
||||
'src/features/truenas/TrueNASProtectionTable.tsx',
|
||||
'src/features/vmware/VmwarePageSurface.tsx',
|
||||
];
|
||||
const localRefreshButtonClass =
|
||||
'inline-flex min-h-10 items-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-surface-hover';
|
||||
|
||||
expect(registeredRule?.canonical?.path).toBe(
|
||||
'src/features/platformPage/sharedPlatformPage.tsx',
|
||||
);
|
||||
expect(registeredRule?.canonical?.export).toBe('PlatformErrorState');
|
||||
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual(
|
||||
expect.arrayContaining(requiredConsumerPaths),
|
||||
);
|
||||
expect(requiredGuard?.canonical?.path).toBe('src/features/platformPage/sharedPlatformPage.tsx');
|
||||
expect(requiredGuard?.canonical?.export).toBe('PlatformErrorState');
|
||||
expect(requiredGuard?.triggerPatterns).toEqual(['title="Could not load']);
|
||||
expect(requiredGuard?.requiredPatterns).toEqual(['PlatformErrorState']);
|
||||
expect(requiredGuard?.allowedPaths ?? []).toHaveLength(0);
|
||||
expect(requiredGuard?.scopes).toEqual(
|
||||
expect.arrayContaining([
|
||||
'src/features/docker',
|
||||
'src/features/kubernetes',
|
||||
'src/features/proxmox',
|
||||
'src/features/standalone',
|
||||
'src/features/truenas',
|
||||
'src/features/vmware',
|
||||
]),
|
||||
);
|
||||
|
||||
expect(sharedPlatformPageSource).toContain('export function PlatformErrorState');
|
||||
expect(sharedPlatformPageSource).toContain('TriangleAlertIcon');
|
||||
expect(sharedPlatformPageSource).toContain('onClick={props.onRefresh}');
|
||||
expect(sharedPlatformPageSource).toContain('Refresh');
|
||||
|
||||
for (const source of [
|
||||
dockerPageSurfaceSource,
|
||||
kubernetesPageSurfaceSource,
|
||||
proxmoxBackupsTableSource,
|
||||
proxmoxPageSurfaceSource,
|
||||
proxmoxReplicationTableSource,
|
||||
standalonePageSurfaceSource,
|
||||
truenasPageSurfaceSource,
|
||||
truenasProtectionTableSource,
|
||||
vmwarePageSurfaceSource,
|
||||
]) {
|
||||
expect(source).toContain('PlatformErrorState');
|
||||
}
|
||||
|
||||
for (const source of [proxmoxBackupsTableSource, proxmoxReplicationTableSource]) {
|
||||
expect(source).not.toContain(localRefreshButtonClass);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps platform row-detail disclosure controls on the shared toggle primitive', () => {
|
||||
const registry = JSON.parse(sharedTemplateRegistrySource) as {
|
||||
requiredPatternGuards?: Array<{
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import { useSearchParams } from '@solidjs/router';
|
|||
import { FilterBar, type FilterDef, type FilterSelectOption } from '@/components/shared/FilterBar';
|
||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||
import { apiFetch } from '@/utils/apiClient';
|
||||
import { PlatformTableEmptyState } from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
PlatformErrorState,
|
||||
PlatformTableLoadingState,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import {
|
||||
getRecoveryFilterDateLabel,
|
||||
recoveryDateKeyFromTimestamp,
|
||||
|
|
@ -437,27 +440,17 @@ export const ProxmoxBackupsTable: Component<{
|
|||
<Show
|
||||
when={!backups.error}
|
||||
fallback={
|
||||
<PlatformTableEmptyState
|
||||
icon={props.emptyIcon}
|
||||
<PlatformErrorState
|
||||
title="Could not load Proxmox backup inventory"
|
||||
description={(backups.error as Error | undefined)?.message ?? 'Refresh to retry.'}
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void refetch()}
|
||||
class="inline-flex min-h-10 items-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-surface-hover"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
}
|
||||
onRefresh={() => void refetch()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={backups() !== undefined}
|
||||
fallback={
|
||||
<PlatformTableEmptyState
|
||||
icon={props.emptyIcon}
|
||||
<PlatformTableLoadingState
|
||||
title="Loading Proxmox backup inventory"
|
||||
description="Reading PBS snapshots, PVE backup files, guest snapshots, and recent backup tasks."
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import { apiFetch } from '@/utils/apiClient';
|
|||
import { formatRelativeTime } from '@/utils/format';
|
||||
import {
|
||||
PlatformTableToolbar,
|
||||
PlatformErrorState,
|
||||
getPlatformTableCellClassForKind,
|
||||
getPlatformTableHeadClassForKind,
|
||||
type PlatformTableFilterOption,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableLoadingState,
|
||||
PlatformTableShell,
|
||||
} from '@/features/platformPage/sharedPlatformPage';
|
||||
import type { ReplicationJob, ReplicationJobsResponse } from '@/types/api';
|
||||
|
|
@ -197,27 +199,17 @@ export const ProxmoxReplicationTable: Component<{
|
|||
<Show
|
||||
when={!props.error}
|
||||
fallback={
|
||||
<PlatformTableEmptyState
|
||||
icon={props.emptyIcon}
|
||||
<PlatformErrorState
|
||||
title="Could not load replication jobs"
|
||||
description={(props.error as Error | undefined)?.message ?? 'Refresh to retry.'}
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onRetry()}
|
||||
class="inline-flex min-h-10 items-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-surface-hover"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
}
|
||||
onRefresh={() => props.onRetry()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={props.jobs !== undefined}
|
||||
fallback={
|
||||
<PlatformTableEmptyState
|
||||
icon={props.emptyIcon}
|
||||
<PlatformTableLoadingState
|
||||
title="Loading replication jobs"
|
||||
description="Reading scheduled replication state from PVE."
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { JSX } from 'solid-js';
|
|||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ProxmoxBackupsTable } from '../ProxmoxBackupsTable';
|
||||
import proxmoxBackupsTableSource from '../ProxmoxBackupsTable.tsx?raw';
|
||||
import {
|
||||
PLATFORM_TABLE_BODY_CLASS,
|
||||
PLATFORM_TABLE_HEADER_ROW_CLASS,
|
||||
|
|
@ -278,4 +279,14 @@ describe('ProxmoxBackupsTable', () => {
|
|||
);
|
||||
expect(emptyStateHeading.closest('.border-dashed')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('routes top-level loading and error states through shared platform primitives', () => {
|
||||
expect(proxmoxBackupsTableSource).toContain('PlatformErrorState');
|
||||
expect(proxmoxBackupsTableSource).toContain('PlatformTableLoadingState');
|
||||
expect(proxmoxBackupsTableSource).toContain('title="Could not load Proxmox backup inventory"');
|
||||
expect(proxmoxBackupsTableSource).toContain('title="Loading Proxmox backup inventory"');
|
||||
expect(proxmoxBackupsTableSource).not.toContain(
|
||||
'inline-flex min-h-10 items-center rounded-md border border-border px-3 py-2 text-sm font-medium hover:bg-surface-hover',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue