Use shared metric bars for Kubernetes clusters

This commit is contained in:
rcourtman
2026-05-20 12:19:11 +01:00
parent 300af43128
commit 92b3116047
2 changed files with 120 additions and 14 deletions
@@ -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 <span class="text-muted">—</span>;
return <span class="tabular-nums">{percent.toFixed(1)}%</span>;
};
const finiteMetric = (value: number | undefined): number | undefined =>
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
const metricFallback = () => (
<div class="flex justify-center">
<span class="text-xs text-muted" aria-hidden="true">
—
</span>
</div>
);
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
/>
<span
class="truncate font-semibold text-base-content"
title={name()}
>
<span class="truncate font-semibold text-base-content" title={name()}>
{name()}
</span>
</div>
@@ -226,7 +240,9 @@ export const KubernetesClustersTable: Component<{
<TableCell
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
{version()}
<span class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 font-mono text-[10px] text-base-content">
{version()}
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
@@ -244,14 +260,30 @@ export const KubernetesClustersTable: Component<{
{counts().deployments}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
{formatPercent(cluster.cpu?.current)}
<ResponsiveMetricCell
class="w-full"
value={cpuPercent() ?? 0}
type="cpu"
resourceId={metricsKey()}
isRunning={canRenderMetrics() && cpuPercent() !== undefined}
showMobile={false}
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
{formatPercent(cluster.memory?.current)}
<Show
when={canRenderMetrics() && hasMemoryMetric()}
fallback={metricFallback()}
>
<StackedMemoryBar
used={memoryUsed()}
total={memoryTotal()}
percentOnly={memoryPercentOnly()}
/>
</Show>
</TableCell>
</TableRow>
<PlatformResourceDetailTableRow
@@ -0,0 +1,74 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Resource } from '@/types/resource';
import { KubernetesClustersTable } from '../KubernetesClustersTable';
vi.mock('@/components/shared/responsive', () => ({
ResponsiveMetricCell: () => <div data-testid="responsive-metric-cell" />,
}));
vi.mock('@/components/Workloads/StackedMemoryBar', () => ({
StackedMemoryBar: () => <div data-testid="stacked-memory-bar" />,
}));
const makeClusterResource = (overrides: Partial<Resource> = {}): 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(() => (
<KubernetesClustersTable
clusters={[cluster]}
scope={[
cluster,
{
...makeClusterResource({
id: 'node-1',
type: 'k8s-node',
name: 'node-1',
kubernetes: {
clusterId: 'prod-west',
clusterName: 'prod-west',
},
}),
},
]}
emptyIcon={<span />}
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();
});
});