mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 16:42:15 +00:00
Use shared metric bars for Kubernetes clusters
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user