k8s(deployments): replace generic infra table with a deployment-native table

The Kubernetes Deployments tab was mounting the canonical
UnifiedResourceTable, which renders CPU / Memory / Disk / Disk I/O /
Uptime / Temperature columns. For Kubernetes Deployments those columns
are conceptually N/A: Deployments are scheduling abstractions over
their controlled pods and do not have temperature sensors, process
uptime, or per-disk I/O of their own. The user's complaint
("uptime/temp/disk-io still dashes — why show columns the data can't
back?") is right: the generic infrastructure table is the wrong UI
for deployments.

Add `KubernetesDeploymentsTable` under
`features/kubernetes/KubernetesDeploymentsTable.tsx` and mount it on
`/kubernetes/deployments` instead of the shared
`PlatformResourceTable`. The new table reuses the canonical shared
primitives (Card, Table, SearchInput, FilterButtonGroup, StatusDot)
plus the shared `filterPlatformResources` helper for search + status,
but surfaces deployment-meaningful columns only:

- Deployment (status dot + name)
- Namespace
- Cluster
- Desired / Updated / Ready / Available replicas (right-aligned,
  tabular-nums)

The operator toolbar (search + status chip strip + counter) and the
empty-state path follow the same shape as `PlatformResourceTable` so
the page still feels native to the platform-page family.

`ResourceKubernetesMeta` extended with `deploymentUid`,
`desiredReplicas`, `updatedReplicas`, `readyReplicas`,
`availableReplicas`. The backend `K8sData` already emits these fields
on `k8s-deployment` rows; the frontend type just hadn't surfaced them
yet (the existing K8sDeploymentsDrawer worked around it with a local
shape).

The synthetic CPU/memory/disk metrics added in 8fb141c0a / 60e649a45
stay on the canonical adapter (other K8s surfaces or future
infrastructure-table uses will benefit) but are no longer the source
of column truth for the platform-page Deployments tab — the table
just doesn't render those columns.

Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass; the every-sub-tab operator-controls audit still
  finds the search input on `/kubernetes/deployments` (now provided
  by the bespoke deployments table's toolbar).

Tests:
- `tsc --noEmit` clean.
- Existing vitest suites continue to pass.

Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set
because this is a presentation refactor inside the Kubernetes feature
directory. No public API shape changes (the backend already emitted
replica fields on `k8s-deployment` resources), no canonical
subsystem boundary changes, no platform-page composition contract
changes; the new bespoke table reuses canonical shared primitives.
This commit is contained in:
rcourtman 2026-05-16 12:23:57 +01:00
parent 60e649a456
commit 69f70a3fcd
3 changed files with 181 additions and 1 deletions

View file

@ -0,0 +1,169 @@
import { For, Show, createMemo, createSignal, type Component, type JSX } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { EmptyState } from '@/components/shared/EmptyState';
import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup';
import { SearchInput } from '@/components/shared/SearchInput';
import { StatusDot } from '@/components/shared/StatusDot';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/shared/Table';
import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
import {
filterPlatformResources,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
// Kubernetes Deployments are scheduling abstractions over their controlled
// pods, so the generic infrastructure table's CPU / Memory / Disk I/O /
// Uptime / Temperature columns are conceptually N/A on these rows and
// render as dashes. This deployment-native table reuses canonical shared
// primitives (Card, Table, SearchInput, FilterButtonGroup, StatusDot) but
// surfaces deployment-meaningful columns only: namespace, cluster,
// desired / updated / ready / available replicas.
const STATUS_FILTER_OPTIONS: FilterOption<PlatformResourceStatusFilter>[] = [
{ value: 'all', label: 'All' },
{ value: 'online', label: 'Healthy' },
{ value: 'degraded', label: 'Degraded' },
{ value: 'offline', label: 'Offline' },
];
const replicaCount = (value: number | undefined): JSX.Element => (
<span class="tabular-nums">{value ?? 0}</span>
);
export const KubernetesDeploymentsTable: Component<{
resources: Resource[];
emptyIcon: JSX.Element;
emptyTitle: string;
emptyDescription: string;
}> = (props) => {
const [search, setSearch] = createSignal('');
const [status, setStatus] = createSignal<PlatformResourceStatusFilter>('all');
const filtered = createMemo(() => filterPlatformResources(props.resources, search(), status()));
const visible = createMemo(() => filtered().length);
const total = createMemo(() => props.resources.length);
return (
<Show
when={props.resources.length > 0}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title={props.emptyTitle}
description={props.emptyDescription}
/>
</Card>
}
>
<div class="space-y-3">
<div class="flex flex-wrap items-center gap-2">
<div class="min-w-[200px] flex-1 sm:max-w-xs">
<SearchInput
value={search}
onChange={setSearch}
placeholder="Search deployments"
/>
</div>
<FilterButtonGroup
options={STATUS_FILTER_OPTIONS}
value={status()}
onChange={setStatus}
/>
<span class="ml-auto whitespace-nowrap text-xs font-medium text-muted">
<Show when={visible() !== total()} fallback={<>{total()} deployments</>}>
{visible()} of {total()} deployments
</Show>
</span>
</div>
<Show
when={filtered().length > 0}
fallback={
<Card padding="lg">
<EmptyState
icon={props.emptyIcon}
title="No deployments match current filters"
description="Adjust the search or status filter to see more deployments."
/>
</Card>
}
>
<Card padding="none" tone="card" class="overflow-hidden">
<Table class="w-full min-w-[820px] border-collapse text-xs">
<TableHeader class="bg-surface-alt text-muted border-b border-border">
<TableRow class="text-left text-[10px] uppercase tracking-wide">
<TableHead class="px-3 py-2 font-medium">Deployment</TableHead>
<TableHead class="px-3 py-2 font-medium">Namespace</TableHead>
<TableHead class="px-3 py-2 font-medium">Cluster</TableHead>
<TableHead class="px-3 py-2 font-medium text-right">Desired</TableHead>
<TableHead class="px-3 py-2 font-medium text-right">Updated</TableHead>
<TableHead class="px-3 py-2 font-medium text-right">Ready</TableHead>
<TableHead class="px-3 py-2 font-medium text-right">Available</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border-subtle">
<For each={filtered()}>
{(deployment) => {
const name = () => asTrimmedString(deployment.name) || deployment.id;
const ns = () => asTrimmedString(deployment.kubernetes?.namespace) || '—';
const cluster = () =>
asTrimmedString(deployment.kubernetes?.clusterName) ||
asTrimmedString(deployment.kubernetes?.clusterId) ||
'—';
const indicator = () => getSimpleStatusIndicator(deployment.status);
return (
<TableRow class="hover:bg-surface-hover">
<TableCell class="px-3 py-2">
<div class="flex items-center gap-2 min-w-0">
<StatusDot
size="sm"
variant={indicator().variant}
title={deployment.status || 'unknown'}
ariaHidden
/>
<span
class="font-semibold text-base-content truncate"
title={name()}
>
{name()}
</span>
</div>
</TableCell>
<TableCell class="px-3 py-2 text-base-content">{ns()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{cluster()}</TableCell>
<TableCell class="px-3 py-2 text-right text-base-content">
{replicaCount(deployment.kubernetes?.desiredReplicas)}
</TableCell>
<TableCell class="px-3 py-2 text-right text-base-content">
{replicaCount(deployment.kubernetes?.updatedReplicas)}
</TableCell>
<TableCell class="px-3 py-2 text-right text-base-content">
{replicaCount(deployment.kubernetes?.readyReplicas)}
</TableCell>
<TableCell class="px-3 py-2 text-right text-base-content">
{replicaCount(deployment.kubernetes?.availableReplicas)}
</TableCell>
</TableRow>
);
}}
</For>
</TableBody>
</Table>
</Card>
</Show>
</div>
</Show>
);
};
export default KubernetesDeploymentsTable;

View file

@ -9,6 +9,7 @@ import {
PlatformSectionTabs,
PlatformTableEmptyState,
} from '@/features/platformPage/sharedPlatformPage';
import { KubernetesDeploymentsTable } from './KubernetesDeploymentsTable';
import {
KUBERNETES_TAB_SPECS,
buildKubernetesPageModel,
@ -108,7 +109,7 @@ export function KubernetesPageSurface() {
/>
</Show>
<Show when={activeTab() === 'deployments'}>
<PlatformResourceTable
<KubernetesDeploymentsTable
resources={model().deployments}
emptyIcon={k8sIcon()}
emptyTitle="No deployments reported"

View file

@ -518,6 +518,16 @@ export interface ResourceKubernetesMeta {
temperature?: number;
pendingUninstall?: boolean;
metricCapabilities?: ResourceKubernetesMetricCapabilities;
// Deployment-only fields populated by the canonical adapter for
// `k8s-deployment` resources. Surfaced on the Kubernetes platform-page
// Deployments table where CPU/memory/uptime/temperature columns from
// the generic infrastructure table are not the meaningful operator
// columns; replica counts are.
deploymentUid?: string;
desiredReplicas?: number;
updatedReplicas?: number;
readyReplicas?: number;
availableReplicas?: number;
}
export interface ResourceVMwareMeta {