Converge platform table value primitives

This commit is contained in:
rcourtman
2026-06-13 11:32:50 +01:00
parent 626c191cde
commit 9ea67c6a6d
13 changed files with 490 additions and 64 deletions
@@ -2613,25 +2613,34 @@ Platform table byte-size formatting follows the same rule.
`formatPlatformTableBytesValue` owns the repeated positive-byte formatting plus
canonical empty-cell marker behavior for dense platform table cells. Docker /
Podman native table helpers, Kubernetes node capacity cells, and TrueNAS
system/VM byte cells must compose that helper instead of declaring local
`formatBytes` wrappers or reimplementing byte-unit precision in table files.
system, VM, storage-topology, and protection byte cells must compose that
helper instead of declaring local `formatBytes` wrappers, importing the generic
formatter in table files, or reimplementing byte-unit precision there.
Platform table numeric fallback rendering is registry-backed too.
`PlatformTableNumberValue` owns finite-number checking, tabular-number styling,
custom empty-marker support, and caller-owned number formatting for dense
optional numeric table cells. Docker / Podman native count helpers, Kubernetes
optional count cells, Docker Swarm service desired/running counts, Kubernetes
Deployment replica counts, and Proxmox Mail Gateway count columns must compose
that primitive instead of declaring local `numberValue`, `numericValue`,
`replicaCount`, or `countCell` helpers. If a scheduler or service-domain count
is intentionally zero-defaulted, the consuming table owns that field/default
choice and still renders through `PlatformTableNumberValue`.
Deployment replica counts, Proxmox Mail Gateway count columns, and TrueNAS
system share/service and storage-topology disk count cells must compose that
primitive instead of declaring local `numberValue`, `numericValue`,
`replicaCount`, `countCell`, `diskCountLabel`, or cell-level `tabular-nums`
variants. If a scheduler, service-domain, or inventory count is intentionally
zero-defaulted, the consuming table owns that field/default choice and still
renders through `PlatformTableNumberValue`.
`PlatformTableCountRatioValue` owns the companion healthy/total or ready/total
count-ratio skeleton: numerator, slash, muted denominator, tabular styling, and
empty marker behavior. Kubernetes cluster child counts compose that primitive
instead of keeping a table-local `childCountCell` renderer; the table owns only
which current/total values and warning tone apply.
One-decimal percent and positive Celsius cells are also shared platform-table
value primitives. `PlatformTablePercentValue` owns percent formatting,
tabular-number styling, and empty markers, while
`PlatformTableTemperatureValue` owns finite positive Celsius validation,
one-decimal `°C` formatting, tabular-number styling, and empty markers. Docker
/ Podman host and TrueNAS system tables must compose those primitives instead
of carrying local `formatPercent` or `formatTemperature` helpers.
/ Podman host and TrueNAS system/storage-topology tables must compose those
primitives instead of carrying local `formatPercent`, `formatTemperature`, or
temperature label helpers.
Platform table metric fallback rendering is also shared.
`PlatformTableMetricFallback` owns the centered muted empty marker used in
metric bar cells plus optional caller-owned fallback label/title text, and
@@ -249,20 +249,25 @@ consumers own whether a capacity, memory, disk, or requested-size field is the
right source-specific value, while dense platform table cells must use
`formatPlatformTableBytesValue` for positive byte formatting and unknown /
non-positive empty-cell markers instead of carrying table-local `formatBytes`
wrappers.
wrappers or direct generic byte-formatter imports.
Optional numeric table cells follow the same split: unified-resource consumers
own which count or replica field is meaningful, whether the domain should
zero-default an absent scheduler/service count, and whether a domain-specific
formatter is needed, while dense platform table cells must use
`PlatformTableNumberValue` for finite-number validation, tabular styling, and
unknown empty-cell markers instead of carrying local `numberValue`,
`numericValue`, `replicaCount`, or `countCell` helpers.
zero-default an absent scheduler/service/inventory count, whether a
domain-specific formatter is needed, and which current/total fields belong in a
grouped health ratio, while dense platform table cells must use
`PlatformTableNumberValue` for scalar count validation, tabular styling, and
unknown empty-cell markers, and `PlatformTableCountRatioValue` for
healthy/total or ready/total count-ratio skeletons. Tables must not carry local
`numberValue`, `numericValue`, `replicaCount`, `countCell`, `childCountCell`,
`diskCountLabel`, or cell-level `tabular-nums` variants for grouped
child/share/service/storage counts.
Percent and temperature table cells follow the same split: unified-resource
consumers own which usage percentage or temperature source is meaningful for
the row, while dense platform table rendering must use
`PlatformTablePercentValue` and `PlatformTableTemperatureValue` for one-decimal
formatting, positive Celsius validation, tabular styling, and empty markers
instead of local `formatPercent` or `formatTemperature` helpers.
instead of local `formatPercent`, `formatTemperature`, or temperature label
helpers.
Metric bar fallbacks follow that split as well: unified-resource consumers own
which CPU or memory value is selected, plus any source-specific fallback reason
such as outdated standalone agent telemetry, while platform tables must use
@@ -1371,9 +1371,6 @@ const HELPER_RULES = [
// The kind-based wrappers internally delegate to the align-based
// helpers; that's the one legitimate call site.
'src/features/platformPage/sharedPlatformPage.tsx',
// KubernetesClustersTable is pending migration by another agent.
// Remove this entry once that work lands.
'src/features/kubernetes/KubernetesClustersTable.tsx',
]),
},
];
@@ -1347,6 +1347,8 @@
"requiredConsumers": [
{ "path": "src/features/docker/DockerNativeTableShared.tsx" },
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" },
{ "path": "src/features/truenas/TrueNASVirtualMachinesTable.tsx" }
],
@@ -1359,6 +1361,18 @@
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
"patterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"]
},
{
"path": "src/features/truenas/TrueNASProtectionTable.tsx",
"patterns": ["formatBytes(point.sizeBytes)"]
},
{
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
"patterns": [
"formatBytes(size)",
"formatBytes(row.resource.disk.used)",
"formatBytes(row.resource.disk.total)"
]
},
{
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
"patterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"]
@@ -1391,7 +1405,9 @@
{ "path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx" },
{ "path": "src/features/kubernetes/KubernetesEventsTable.tsx" },
{ "path": "src/features/kubernetes/KubernetesPodsTable.tsx" },
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" }
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" },
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }
],
"forbiddenPatterns": [
{
@@ -1446,6 +1462,35 @@
{
"path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx",
"patterns": ["const countCell", "value.toLocaleString() : '—'"]
},
{
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
"patterns": ["hidden text-base-content tabular-nums lg:table-cell"]
},
{
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
"patterns": ["const diskCountLabel"]
}
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-count-ratio-value",
"category": "platform-table-text-value",
"summary": "Platform tables must use the shared count-ratio primitive for healthy/total or ready/total count cells instead of recreating local numerator/slash/denominator markup.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "PlatformTableCountRatioValue"
},
"requiredConsumers": [{ "path": "src/features/kubernetes/KubernetesClustersTable.tsx" }],
"forbiddenPatterns": [
{
"path": "src/features/kubernetes/KubernetesClustersTable.tsx",
"patterns": ["const childCountCell", "<span class=\"text-muted\">/{count.total}</span>"]
}
],
"proof": [
@@ -1488,6 +1533,7 @@
},
"requiredConsumers": [
{ "path": "src/features/docker/DockerHostsTable.tsx" },
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }
],
"forbiddenPatterns": [
@@ -1498,6 +1544,10 @@
"<span class=\"tabular-nums\">{celsius.toFixed(1)}°C</span>"
]
},
{
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
"patterns": ["const temperatureLabel", "${Math.round(value)}C"]
},
{
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
"patterns": [
@@ -2108,6 +2158,26 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-format-bytes-import",
"category": "platform-table-text-value",
"summary": "Platform feature tables must not import the generic byte formatter directly for dense table byte cells.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "formatPlatformTableBytesValue"
},
"scopes": ["src/features/truenas"],
"extensions": [".tsx"],
"allPatterns": ["import { formatBytes } from '@/utils/format';", "formatBytes("],
"legacyReason": "Retired migration debt. Dense platform table byte cells must use formatPlatformTableBytesValue so positive-byte formatting and empty markers stay shared.",
"allowedPaths": [],
"ignoredPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-number-value-helper",
"category": "platform-table-text-value",
@@ -2188,6 +2258,46 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-count-ratio-helper",
"category": "platform-table-text-value",
"summary": "Platform tables must not recreate local healthy/total count-ratio helpers.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "PlatformTableCountRatioValue"
},
"scopes": ["src/features/kubernetes"],
"extensions": [".tsx"],
"allPatterns": ["const childCountCell", "<span class=\"text-muted\">/{count.total}</span>"],
"legacyReason": "Retired migration debt. Healthy/total and ready/total table count cells must compose PlatformTableCountRatioValue so ratio markup, tabular styling, muted denominator styling, and empty markers stay shared.",
"allowedPaths": [],
"ignoredPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-truenas-count-cell-class",
"category": "platform-table-text-value",
"summary": "TrueNAS table scalar value cells must not put tabular-number styling on table cells instead of shared value primitives.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "PlatformTableNumberValue"
},
"scopes": ["src/features/truenas"],
"extensions": [".tsx"],
"allPatterns": ["TrueNAS", "hidden text-base-content tabular-nums lg:table-cell"],
"legacyReason": "Retired migration debt. TrueNAS share/service and adjacent scalar value cells must render through shared platform value primitives so finite-number checks, tabular styling, and empty markers stay shared.",
"allowedPaths": [],
"ignoredPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-percent-value-helper",
"category": "platform-table-text-value",
@@ -2234,6 +2344,26 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-temperature-label-helper",
"category": "platform-table-text-value",
"summary": "Platform tables must not recreate local temperatureLabel helpers for positive Celsius cells.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "PlatformTableTemperatureValue"
},
"scopes": ["src/features/truenas"],
"extensions": [".tsx"],
"allPatterns": ["const temperatureLabel", "${Math.round(value)}C"],
"legacyReason": "Retired migration debt. Positive Celsius table cells must compose PlatformTableTemperatureValue so validity, formatting, tabular styling, and empty markers stay shared.",
"allowedPaths": [],
"ignoredPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-local-metric-fallback-helper",
"category": "platform-table-metric",
@@ -3689,9 +3689,14 @@ describe('shared primitive guardrails', () => {
const localHelperGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-bytes-helper',
);
const localFormatBytesImportGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-format-bytes-import',
);
const platformByteValueConsumers: Array<[string, string]> = [
['src/features/docker/DockerNativeTableShared.tsx', dockerNativeTableSharedSource],
['src/features/kubernetes/KubernetesNodesTable.tsx', kubernetesNodesTableSource],
['src/features/truenas/TrueNASProtectionTable.tsx', truenasProtectionTableSource],
['src/features/truenas/TrueNASStorageTopologyTable.tsx', truenasStorageTopologyTableSource],
['src/features/truenas/TrueNASSystemsTable.tsx', truenasSystemsTableSource],
['src/features/truenas/TrueNASVirtualMachinesTable.tsx', truenasVirtualMachinesTableSource],
];
@@ -3717,6 +3722,18 @@ describe('shared primitive guardrails', () => {
path: 'src/features/kubernetes/KubernetesNodesTable.tsx',
patterns: localFormatBytesHelperPatterns,
},
{
path: 'src/features/truenas/TrueNASProtectionTable.tsx',
patterns: ['formatBytes(point.sizeBytes)'],
},
{
path: 'src/features/truenas/TrueNASStorageTopologyTable.tsx',
patterns: [
'formatBytes(size)',
'formatBytes(row.resource.disk.used)',
'formatBytes(row.resource.disk.total)',
],
},
{
path: 'src/features/truenas/TrueNASSystemsTable.tsx',
patterns: localFormatBytesHelperPatterns,
@@ -3738,6 +3755,17 @@ describe('shared primitive guardrails', () => {
]);
expect(localHelperGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localHelperGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(localFormatBytesImportGuard?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(localFormatBytesImportGuard?.canonical?.export).toBe('formatPlatformTableBytesValue');
expect(localFormatBytesImportGuard?.allPatterns).toEqual([
"import { formatBytes } from '@/utils/format';",
'formatBytes(',
]);
expect(localFormatBytesImportGuard?.scopes).toEqual(['src/features/truenas']);
expect(localFormatBytesImportGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localFormatBytesImportGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(sharedPlatformPageSource).toContain('export const formatPlatformTableBytesValue');
expect(sharedPlatformPageSource).toContain('formatBytes(bytes)');
@@ -3787,6 +3815,9 @@ describe('shared primitive guardrails', () => {
const localSwarmCountGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-swarm-count-spans',
);
const localTrueNASCountGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-truenas-count-cell-class',
);
const platformNumberValueConsumers: Array<[string, string]> = [
['src/features/docker/DockerNativeTableShared.tsx', dockerNativeTableSharedSource],
['src/features/docker/DockerServicesTable.tsx', dockerServicesTableSource],
@@ -3797,6 +3828,8 @@ describe('shared primitive guardrails', () => {
['src/features/kubernetes/KubernetesEventsTable.tsx', kubernetesEventsTableSource],
['src/features/kubernetes/KubernetesPodsTable.tsx', kubernetesPodsTableSource],
['src/features/proxmox/ProxmoxMailGatewayTable.tsx', proxmoxMailGatewayTableSource],
['src/features/truenas/TrueNASStorageTopologyTable.tsx', truenasStorageTopologyTableSource],
['src/features/truenas/TrueNASSystemsTable.tsx', truenasSystemsTableSource],
];
const platformNumberValueConsumerPaths = platformNumberValueConsumers.map(([path]) => path);
const optionalNumberMarkupPattern =
@@ -3849,6 +3882,14 @@ describe('shared primitive guardrails', () => {
path: 'src/features/proxmox/ProxmoxMailGatewayTable.tsx',
patterns: ['const countCell', "value.toLocaleString() : '—'"],
},
{
path: 'src/features/truenas/TrueNASSystemsTable.tsx',
patterns: ['hidden text-base-content tabular-nums lg:table-cell'],
},
{
path: 'src/features/truenas/TrueNASStorageTopologyTable.tsx',
patterns: ['const diskCountLabel'],
},
]);
expect(localNumberGuard?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
@@ -3898,6 +3939,17 @@ describe('shared primitive guardrails', () => {
expect(localSwarmCountGuard?.scopes).toEqual(['src/components/Docker']);
expect(localSwarmCountGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localSwarmCountGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(localTrueNASCountGuard?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(localTrueNASCountGuard?.canonical?.export).toBe('PlatformTableNumberValue');
expect(localTrueNASCountGuard?.allPatterns).toEqual([
'TrueNAS',
'hidden text-base-content tabular-nums lg:table-cell',
]);
expect(localTrueNASCountGuard?.scopes).toEqual(['src/features/truenas']);
expect(localTrueNASCountGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localTrueNASCountGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(sharedPlatformPageSource).toContain('export function PlatformTableNumberValue');
expect(sharedPlatformPageSource).toContain('Number.isFinite(value)');
@@ -3914,6 +3966,71 @@ describe('shared primitive guardrails', () => {
expect(proxmoxMailGatewayTableSource).toContain('format={formatLocaleCount}');
});
it('keeps platform table count ratios on the shared primitive', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
rules?: Array<{
id: string;
canonical?: { path?: string; export?: string };
requiredConsumers?: Array<{ path?: string }>;
forbiddenPatterns?: Array<{ path?: string; patterns?: string[] }>;
}>;
patternGuards?: Array<{
id: string;
canonical?: { path?: string; export?: string };
allPatterns?: string[];
scopes?: string[];
allowedPaths?: string[];
ignoredPaths?: string[];
}>;
};
const registeredRule = registry.rules?.find(
(rule) => rule.id === 'platform-table-count-ratio-value',
);
const localCountRatioGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-count-ratio-helper',
);
const platformCountRatioConsumers: Array<[string, string]> = [
['src/features/kubernetes/KubernetesClustersTable.tsx', kubernetesClustersTableSource],
];
const platformCountRatioConsumerPaths = platformCountRatioConsumers.map(([path]) => path);
expect(registeredRule?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(registeredRule?.canonical?.export).toBe('PlatformTableCountRatioValue');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual(
platformCountRatioConsumerPaths,
);
expect(registeredRule?.forbiddenPatterns).toEqual([
{
path: 'src/features/kubernetes/KubernetesClustersTable.tsx',
patterns: ['const childCountCell', '<span class="text-muted">/{count.total}</span>'],
},
]);
expect(localCountRatioGuard?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(localCountRatioGuard?.canonical?.export).toBe('PlatformTableCountRatioValue');
expect(localCountRatioGuard?.allPatterns).toEqual([
'const childCountCell',
'<span class="text-muted">/{count.total}</span>',
]);
expect(localCountRatioGuard?.scopes).toEqual(['src/features/kubernetes']);
expect(localCountRatioGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localCountRatioGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(sharedPlatformPageSource).toContain('export function PlatformTableCountRatioValue');
for (const [path, source] of platformCountRatioConsumers) {
expect(source).toContain('PlatformTableCountRatioValue');
const forbiddenPatterns =
registeredRule?.forbiddenPatterns?.find((entry) => entry.path === path)?.patterns ?? [];
for (const pattern of forbiddenPatterns) {
expect(source).not.toContain(pattern);
}
}
});
it('keeps platform table scalar unit values on shared primitives', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
rules?: Array<{
@@ -3943,11 +4060,15 @@ describe('shared primitive guardrails', () => {
const localTemperatureGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-temperature-value-helper',
);
const localTemperatureLabelGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-local-temperature-label-helper',
);
const percentConsumers: Array<[string, string]> = [
['src/features/truenas/TrueNASSystemsTable.tsx', truenasSystemsTableSource],
];
const temperatureConsumers: Array<[string, string]> = [
['src/features/docker/DockerHostsTable.tsx', dockerHostsTableSource],
['src/features/truenas/TrueNASStorageTopologyTable.tsx', truenasStorageTopologyTableSource],
['src/features/truenas/TrueNASSystemsTable.tsx', truenasSystemsTableSource],
];
const localPercentPatterns = [
@@ -3983,6 +4104,10 @@ describe('shared primitive guardrails', () => {
path: 'src/features/docker/DockerHostsTable.tsx',
patterns: localTemperaturePatterns,
},
{
path: 'src/features/truenas/TrueNASStorageTopologyTable.tsx',
patterns: ['const temperatureLabel', '${Math.round(value)}C'],
},
{
path: 'src/features/truenas/TrueNASSystemsTable.tsx',
patterns: localTemperaturePatterns,
@@ -4005,6 +4130,17 @@ describe('shared primitive guardrails', () => {
expect(localTemperatureGuard?.scopes).toEqual(['src/features/docker', 'src/features/truenas']);
expect(localTemperatureGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localTemperatureGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(localTemperatureLabelGuard?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(localTemperatureLabelGuard?.canonical?.export).toBe('PlatformTableTemperatureValue');
expect(localTemperatureLabelGuard?.allPatterns).toEqual([
'const temperatureLabel',
'${Math.round(value)}C',
]);
expect(localTemperatureLabelGuard?.scopes).toEqual(['src/features/truenas']);
expect(localTemperatureLabelGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localTemperatureLabelGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(sharedPlatformPageSource).toContain('export function PlatformTablePercentValue');
expect(sharedPlatformPageSource).toContain('export function PlatformTableTemperatureValue');
@@ -8,6 +8,7 @@ import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
import {
PLATFORM_HEALTH_FILTER_OPTIONS,
PlatformTableCountRatioValue,
PlatformTableMetricFallback,
PlatformTableToolbar,
PlatformTableEmptyState,
@@ -30,7 +31,6 @@ import {
buildKubernetesClusterChildCounts,
emptyKubernetesClusterChildCounts,
filterKubernetesResources,
type KubernetesClusterChildCount,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -44,18 +44,6 @@ import {
// CPU/Memory utilisation. It reuses the same shared primitives every
// other platform-page table uses.
// v5-style healthy/total fraction: the healthy share turns amber as soon as
// any child needs attention, so a cluster with a NotReady node reads "2/3"
// instead of a flat "3".
const childCountCell = (count: KubernetesClusterChildCount) => (
<>
<span class={count.attention > 0 ? 'text-amber-700 dark:text-amber-300' : ''}>
{count.total - count.attention}
</span>
<span class="text-muted">/{count.total}</span>
</>
);
export const KubernetesClustersTable: Component<{
clusters: Resource[];
// All Kubernetes-tagged resources from the same query, so the table can
@@ -231,19 +219,33 @@ export const KubernetesClustersTable: Component<{
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
{childCountCell(counts().nodes)}
<PlatformTableCountRatioValue
current={counts().nodes.total - counts().nodes.attention}
total={counts().nodes.total}
currentTone={counts().nodes.attention > 0 ? 'warning' : undefined}
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{childCountCell(counts().pods)}
<PlatformTableCountRatioValue
current={counts().pods.total - counts().pods.attention}
total={counts().pods.total}
currentTone={counts().pods.attention > 0 ? 'warning' : undefined}
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{childCountCell(counts().deployments)}
<PlatformTableCountRatioValue
current={counts().deployments.total - counts().deployments.attention}
total={counts().deployments.total}
currentTone={
counts().deployments.attention > 0 ? 'warning' : undefined
}
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
@@ -73,6 +73,54 @@ describe('KubernetesClustersTable', () => {
expect(screen.queryByText('Pending uninstall')).toBeNull();
});
it('renders child counts through the shared count-ratio primitive', () => {
const cluster = makeClusterResource();
const { container } = render(() => (
<KubernetesClustersTable
clusters={[cluster]}
scope={[
cluster,
{
...makeClusterResource({
id: 'node-ready',
type: 'k8s-node',
name: 'node-ready',
status: 'online',
kubernetes: {
clusterId: 'prod-west',
clusterName: 'prod-west',
ready: true,
},
}),
},
{
...makeClusterResource({
id: 'node-not-ready',
type: 'k8s-node',
name: 'node-not-ready',
status: 'offline',
kubernetes: {
clusterId: 'prod-west',
clusterName: 'prod-west',
ready: false,
},
}),
},
]}
emptyIcon={<span />}
emptyTitle="No clusters"
emptyDescription="No clusters"
showToolbar={false}
/>
));
const row = container.querySelector('[data-kubernetes-cluster-row="cluster:prod-west"]');
expect(row?.textContent).toContain('1/2');
expect(row?.querySelector('.text-amber-700')?.textContent).toBe('1');
expect(row?.querySelectorAll('.tabular-nums').length).toBeGreaterThanOrEqual(2);
});
it('badges clusters whose agent is pending uninstall', () => {
const cluster = makeClusterResource({
kubernetes: {
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from 'vitest';
import { createRoot, createSignal } from 'solid-js';
import type { Resource } from '@/types/resource';
import {
PlatformTableCountRatioValue,
PlatformTableMetricFallback,
PlatformTableNumberValue,
PlatformTablePercentValue,
@@ -333,6 +334,37 @@ describe('PlatformTableNumberValue', () => {
});
});
describe('PlatformTableCountRatioValue', () => {
it('renders count ratios through shared tabular number styling', () => {
const { container } = render(() =>
PlatformTableCountRatioValue({
current: 2,
total: 3,
currentTone: 'warning',
suffix: 'ready',
}),
);
expect(container.textContent).toBe('2/3 ready');
expect(container.querySelectorAll('.tabular-nums')).toHaveLength(2);
expect(screen.getByText('2').parentElement?.classList.contains('text-amber-700')).toBe(true);
expect(screen.getByText('/').classList.contains('text-muted')).toBe(true);
expect(screen.getByText('3').parentElement?.classList.contains('text-muted')).toBe(true);
});
it('uses the shared empty marker when neither side is finite', () => {
render(() =>
PlatformTableCountRatioValue({
current: undefined,
total: Number.NaN,
emptyText: '-',
}),
);
expect(screen.getByText('-')).toBeInTheDocument();
});
});
describe('PlatformTablePercentValue', () => {
it('renders finite percentages with one decimal place and shared tabular styling', () => {
const { container } = render(() => PlatformTablePercentValue({ value: 42.345 }));
@@ -221,6 +221,41 @@ export function PlatformTableNumberValue(props: {
return <span class="tabular-nums">{label()}</span>;
}
export function PlatformTableCountRatioValue(props: {
current: number | undefined;
total: number | undefined;
currentTone?: 'warning';
emptyText?: string;
suffix?: string;
}) {
const hasAnyValue = () =>
[props.current, props.total].some(
(value) => typeof value === 'number' && Number.isFinite(value),
);
const currentClass = () =>
props.currentTone === 'warning' ? 'text-amber-700 dark:text-amber-300' : '';
return (
<Show
when={hasAnyValue()}
fallback={<PlatformTableNumberValue value={undefined} emptyText={props.emptyText} />}
>
<span class="inline-flex items-baseline whitespace-nowrap">
<span class={currentClass()}>
<PlatformTableNumberValue value={props.current} emptyText={props.emptyText} />
</span>
<span class="text-muted">/</span>
<span class="text-muted">
<PlatformTableNumberValue value={props.total} emptyText={props.emptyText} />
</span>
<Show when={props.suffix}>
{(suffix) => <span class="ml-1 text-muted"> {suffix()}</span>}
</Show>
</span>
</Show>
);
}
const formatOneDecimalPercent = (value: number): string => `${value.toFixed(1)}%`;
const formatOneDecimalCelsius = (value: number): string => `${value.toFixed(1)}°C`;
@@ -2,7 +2,6 @@ import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
import { InlineDetailTableRow } from '@/components/shared/InlineDetailTableRow';
import { StatusDot } from '@/components/shared/StatusDot';
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
import { formatBytes } from '@/utils/format';
import {
getRecoveryOutcomeBadgeClass,
getRecoveryOutcomeLabel,
@@ -16,6 +15,7 @@ import {
PlatformTableLoadingState,
PlatformTableToolbar,
createPlatformTableFilterState,
formatPlatformTableBytesValue,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformTableFilterOption,
@@ -174,7 +174,7 @@ const formatPointTime = (point: RecoveryPoint): string => {
};
const sizeLabel = (point: RecoveryPoint): string =>
typeof point.sizeBytes === 'number' && point.sizeBytes > 0 ? formatBytes(point.sizeBytes) : '-';
formatPlatformTableBytesValue(point.sizeBytes ?? undefined, '-');
type ProtectionDetailTone = DetailValueTone;
type ProtectionDetailSection = DetailSection;
@@ -483,7 +483,7 @@ export const TrueNASProtectionTable: Component<{
{formatPointTime(point)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums lg:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content lg:table-cell`}
>
{sizeLabel(point)}
</TableCell>
@@ -3,14 +3,16 @@ import { StatusDot } from '@/components/shared/StatusDot';
import { ResponsiveMetricCell } from '@/components/shared/responsive';
import { TableCell, TableHead, TableRow } from '@/components/shared/Table';
import { filterChipStatusDot } from '@/components/shared/FilterBar';
import { formatBytes } from '@/utils/format';
import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
import {
PlatformTableEmptyState,
PlatformTableNumberValue,
PlatformTableTemperatureValue,
PlatformTableToolbar,
createPlatformTableFilterState,
formatPlatformTableBytesValue,
formatPlatformTableTitleCaseValue,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
@@ -88,12 +90,12 @@ const capacityPercent = (resource: Resource): number | undefined => {
const diskSizeLabel = (row: TrueNASStorageTopologyRow): string => {
const size = row.resource.physicalDisk?.sizeBytes;
return typeof size === 'number' && size > 0 ? formatBytes(size) : '-';
return formatPlatformTableBytesValue(size, '-');
};
const capacitySublabel = (row: TrueNASStorageTopologyRow): string | undefined => {
if (typeof row.resource.disk?.used === 'number' && typeof row.resource.disk?.total === 'number') {
return `${formatBytes(row.resource.disk.used)} / ${formatBytes(row.resource.disk.total)}`;
return `${formatPlatformTableBytesValue(row.resource.disk.used, '-')} / ${formatPlatformTableBytesValue(row.resource.disk.total, '-')}`;
}
return undefined;
};
@@ -160,17 +162,6 @@ const RiskPill: Component<{ row: TrueNASStorageTopologyRow }> = (props) => (
</span>
);
const temperatureLabel = (row: TrueNASStorageTopologyRow): string => {
if (row.kind !== 'disk') return '-';
const value = row.resource.physicalDisk?.temperature;
return typeof value === 'number' && Number.isFinite(value) ? `${Math.round(value)}C` : '-';
};
const diskCountLabel = (row: TrueNASStorageTopologyRow): string => {
if (row.kind === 'pool') return String(row.counts.disks);
return '-';
};
export const getTrueNASStorageTopologyIndentClass = (depth: number): string => {
if (depth <= 0) return '';
if (depth === 1) return 'pl-5 sm:pl-7';
@@ -328,14 +319,24 @@ export const TrueNASStorageTopologyTable: Component<{
<CapacityCell row={row} />
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{diskCountLabel(row)}
<PlatformTableNumberValue
value={row.kind === 'pool' ? row.counts.disks : undefined}
emptyText="-"
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums lg:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content lg:table-cell`}
>
{temperatureLabel(row)}
<PlatformTableTemperatureValue
value={
row.kind === 'disk'
? row.resource.physicalDisk?.temperature
: undefined
}
emptyText="-"
/>
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('badge')}>
<RiskPill row={row} />
@@ -9,6 +9,7 @@ import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
import {
PLATFORM_HEALTH_FILTER_OPTIONS,
PlatformTableMetricFallback,
PlatformTableNumberValue,
PlatformTablePercentValue,
PlatformTableTemperatureValue,
PlatformTableToolbar,
@@ -322,9 +323,9 @@ export const TrueNASSystemsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums lg:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content lg:table-cell`}
>
{c().shares}
<PlatformTableNumberValue value={c().shares} />
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden whitespace-normal text-base-content lg:table-cell`}
@@ -338,9 +339,9 @@ export const TrueNASSystemsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums lg:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content lg:table-cell`}
>
{c().services}
<PlatformTableNumberValue value={c().services} />
</TableCell>
</TableRow>
<PlatformResourceDetailTableRow
@@ -62,6 +62,36 @@ describe('Resource Type Guards', () => {
]);
});
it('keeps Kubernetes native API sections in the governed platform projection', () => {
expect(SOURCE_PLATFORM_CANONICAL_PROJECTIONS.kubernetes).toEqual([
'k8s-cluster',
'k8s-node',
'pod',
'k8s-deployment',
'k8s-replicaset',
'k8s-statefulset',
'k8s-daemonset',
'k8s-service',
'k8s-job',
'k8s-cronjob',
'k8s-ingress',
'k8s-endpoint-slice',
'k8s-network-policy',
'k8s-persistent-volume',
'k8s-persistent-volume-claim',
'k8s-storage-class',
'k8s-namespace',
'k8s-configmap',
'k8s-secret',
'k8s-serviceaccount',
'k8s-resource-quota',
'k8s-limit-range',
'k8s-pod-disruption-budget',
'k8s-horizontal-pod-autoscaler',
'k8s-event',
]);
});
describe('isInfrastructure', () => {
const infrastructureTypes: ResourceType[] = [
'agent',