diff --git a/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx index 0fdcfa5e2e..7140bcf441 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx @@ -1,7 +1,9 @@ import { For, Show, createMemo, type Component, type JSX } from 'solid-js'; import { StatusDot } from '@/components/shared/StatusDot'; +import { ResponsiveMetricCell } from '@/components/shared/responsive'; import { TableCard } from '@/components/shared/TableCard'; import { TableCardHeader } from '@/components/shared/TableCardHeader'; +import { StackedMemoryBar } from '@/components/Workloads/StackedMemoryBar'; import { Table, TableBody, @@ -10,6 +12,7 @@ import { TableHeader, TableRow, } from '@/components/shared/Table'; +import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys'; import { getSimpleStatusIndicator } from '@/utils/status'; import { asTrimmedString } from '@/utils/stringUtils'; import { @@ -43,11 +46,16 @@ import type { Resource } from '@/types/resource'; // CPU/Memory utilisation. It reuses the same shared primitives every // other platform-page table uses. -const formatPercent = (percent?: number): JSX.Element => { - if (typeof percent !== 'number' || Number.isNaN(percent)) - return —; - return {percent.toFixed(1)}%; -}; +const finiteMetric = (value: number | undefined): number | undefined => + typeof value === 'number' && Number.isFinite(value) ? value : undefined; + +const metricFallback = () => ( +
+ +
+); export const KubernetesClustersTable: Component<{ clusters: Resource[]; @@ -182,6 +190,14 @@ export const KubernetesClustersTable: Component<{ cluster.id; const context = () => asTrimmedString(cluster.kubernetes?.context) || '—'; const version = () => asTrimmedString(cluster.kubernetes?.version) || '—'; + const metricsKey = () => buildMetricKeyForUnifiedResource(cluster); + const cpuPercent = () => finiteMetric(cluster.cpu?.current); + const memoryTotal = () => finiteMetric(cluster.memory?.total) ?? 0; + const memoryUsed = () => finiteMetric(cluster.memory?.used) ?? 0; + const memoryPercentOnly = () => + memoryTotal() > 0 ? undefined : finiteMetric(cluster.memory?.current); + const hasMemoryMetric = () => + memoryTotal() > 0 || memoryPercentOnly() !== undefined; const counts = () => countsByCluster().get(cluster.id) ?? { nodes: 0, @@ -189,6 +205,7 @@ export const KubernetesClustersTable: Component<{ deployments: 0, }; const indicator = () => getSimpleStatusIndicator(cluster.status); + const canRenderMetrics = () => indicator().variant !== 'muted'; const detailRowId = () => drawer.detailRowId(cluster); const isExpanded = () => drawer.isExpanded(cluster); return ( @@ -210,10 +227,7 @@ export const KubernetesClustersTable: Component<{ title={cluster.status || 'unknown'} ariaHidden /> - + {name()} @@ -226,7 +240,9 @@ export const KubernetesClustersTable: Component<{ - {formatPercent(cluster.cpu?.current)} + - {formatPercent(cluster.memory?.current)} + + + ({ + ResponsiveMetricCell: () =>
, +})); + +vi.mock('@/components/Workloads/StackedMemoryBar', () => ({ + StackedMemoryBar: () =>
, +})); + +const makeClusterResource = (overrides: Partial = {}): Resource => ({ + id: 'cluster:prod-west', + name: 'prod-west', + displayName: 'prod-west', + platformId: 'homelab', + platformType: 'kubernetes', + sourceType: 'hybrid', + status: 'online', + type: 'k8s-cluster', + lastSeen: 1_700_000_000_000, + cpu: { current: 42 }, + memory: { total: 32_000, used: 20_000, free: 12_000, current: 62.5 }, + kubernetes: { + clusterId: 'prod-west', + clusterName: 'prod-west', + context: 'prod-west-admin', + version: 'v1.31.3', + server: 'https://prod-west.example.invalid', + }, + ...overrides, +}); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe('KubernetesClustersTable', () => { + it('renders canonical CPU and memory metric primitives for cluster rows', () => { + const cluster = makeClusterResource(); + + render(() => ( + } + emptyTitle="No clusters" + emptyDescription="No clusters" + showToolbar={false} + /> + )); + + expect(screen.getByText('prod-west')).toBeInTheDocument(); + expect(screen.getByTestId('responsive-metric-cell')).toBeInTheDocument(); + expect(screen.getByTestId('stacked-memory-bar')).toBeInTheDocument(); + }); +});