Normalize Kubernetes drawer tables

This commit is contained in:
rcourtman
2026-05-20 13:20:21 +01:00
parent daa155c2d2
commit 74ec866498
4 changed files with 117 additions and 35 deletions
@@ -16,6 +16,12 @@ import {
} from '@/components/shared/Table';
import { EmptyState } from '@/components/shared/EmptyState';
import { buildKubernetesPath } from '@/routing/resourceLinks';
import {
PLATFORM_TABLE_BODY_CLASS,
PLATFORM_TABLE_HEADER_ROW_CLASS,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import {
getK8sDeploymentsDrawerPresentation,
getK8sDeploymentsEmptyState,
@@ -224,33 +230,41 @@ export const K8sDeploymentsDrawer: Component<{
}
>
<Card padding="none" tone="card" class="overflow-hidden">
<Table class="w-full min-w-[760px] 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">
<Table class="min-w-full table-fixed text-xs md:min-w-[820px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[26%]`}>
{drawerPresentation.deploymentColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[16%]`}>
{drawerPresentation.namespaceColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[10%]`}
>
{drawerPresentation.desiredColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[10%]`}
>
{drawerPresentation.updatedColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[10%]`}
>
{drawerPresentation.readyColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[10%]`}
>
{drawerPresentation.availableColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[18%]`}>
{drawerPresentation.actionsColumnLabel}
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border-subtle">
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
<For each={filteredDeployments()}>
{(dep) => {
const name = () => asTrimmedString(dep.name) || dep.id;
@@ -262,8 +276,8 @@ export const K8sDeploymentsDrawer: Component<{
const status = () => getSimpleStatusIndicator(dep.status);
return (
<TableRow class="hover:bg-surface-hover">
<TableCell class="px-3 py-2">
<TableRow class="text-[11px] sm:text-xs">
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex items-center gap-2 min-w-0">
<StatusDot
size="sm"
@@ -276,12 +290,34 @@ export const K8sDeploymentsDrawer: Component<{
</span>
</div>
</TableCell>
<TableCell class="px-3 py-2 text-base-content">{ns()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{desired()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{updated()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{ready()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{available()}</TableCell>
<TableCell class="px-3 py-2">
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span class="block truncate" title={ns()}>
{ns()}
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{desired()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{updated()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{ready()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{available()}
</TableCell>
<TableCell class={getPlatformTableCellClassForKind('text')}>
<button
type="button"
onClick={() => openPods(dep.kubernetes?.namespace)}
@@ -15,6 +15,12 @@ import {
import { EmptyState } from '@/components/shared/EmptyState';
import { StatusDot } from '@/components/shared/StatusDot';
import { buildKubernetesPath } from '@/routing/resourceLinks';
import {
PLATFORM_TABLE_BODY_CLASS,
PLATFORM_TABLE_HEADER_ROW_CLASS,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import {
getK8sNamespacesDrawerPresentation,
getK8sNamespacesEmptyState,
@@ -144,30 +150,34 @@ export const K8sNamespacesDrawer: Component<{
}
>
<Card padding="none" tone="card" class="overflow-hidden">
<Table class="w-full min-w-[720px] 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">
<Table class="min-w-full table-fixed text-xs md:min-w-[760px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[22%]`}>
{drawerPresentation.namespaceColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[18%]`}
>
{drawerPresentation.podsColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[22%]`}
>
{drawerPresentation.deploymentsColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[38%]`}>
{drawerPresentation.actionsColumnLabel}
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border-subtle">
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
<For each={filteredRows()}>
{(row) => {
const podIndicator = () => getNamespaceCountsIndicator(row.pods);
return (
<TableRow class="hover:bg-surface-hover">
<TableCell class="px-3 py-2">
<TableRow class="text-[11px] sm:text-xs">
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex items-center gap-2 min-w-0">
<StatusDot
size="sm"
@@ -183,8 +193,12 @@ export const K8sNamespacesDrawer: Component<{
</span>
</div>
</TableCell>
<TableCell class="px-3 py-2 text-base-content">
<span class="font-semibold">{formatInteger(row.pods.total)}</span>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<span class="font-semibold tabular-nums">
{formatInteger(row.pods.total)}
</span>
<span class="ml-2 text-[11px] text-muted">
{row.pods.offline > 0 ? `${formatInteger(row.pods.offline)} off` : ''}
{row.pods.warning > 0
@@ -192,8 +206,10 @@ export const K8sNamespacesDrawer: Component<{
: ''}
</span>
</TableCell>
<TableCell class="px-3 py-2 text-base-content">
<span class="font-semibold">
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<span class="font-semibold tabular-nums">
{formatInteger(row.deployments.total)}
</span>
<span class="ml-2 text-[11px] text-muted">
@@ -205,7 +221,7 @@ export const K8sNamespacesDrawer: Component<{
: ''}
</span>
</TableCell>
<TableCell class="px-3 py-2">
<TableCell class={getPlatformTableCellClassForKind('text')}>
<div class="flex flex-wrap items-center gap-2">
<button
type="button"
@@ -36,7 +36,7 @@ vi.mock('@/components/shared/Table', () => ({
{props.children}
</table>
),
TableHeader: (props: any) => <thead>{props.children}</thead>,
TableHeader: (props: any) => <thead class={props.class}>{props.children}</thead>,
TableBody: (props: any) => <tbody class={props.class}>{props.children}</tbody>,
TableRow: (props: any) => <tr class={props.class}>{props.children}</tr>,
TableHead: (props: any) => <th class={props.class}>{props.children}</th>,
@@ -190,6 +190,21 @@ describe('K8sDeploymentsDrawer', () => {
expect(screen.getByText('2')).toBeInTheDocument();
});
it('uses the shared platform table spacing and divider contract', async () => {
mockApiResponse([makeDeployment({ id: 'dep-1', name: 'web-frontend' })]);
const { container } = render(() => <K8sDeploymentsDrawer cluster="c1" />);
await waitFor(() => {
expect(screen.getByText('web-frontend')).toBeInTheDocument();
});
expect(screen.getByTestId('table')).toHaveClass('table-fixed');
expect(container.querySelector('thead tr')).toHaveClass('bg-surface-alt');
expect(container.querySelector('tbody')).toHaveClass('divide-y', 'divide-border');
expect(container.querySelector('tbody')).not.toHaveClass('divide-border-subtle');
expect(container.querySelector('th')).toHaveClass('px-1.5', 'py-0.5');
});
it('uses deployment id as name when name is empty', async () => {
const dep = makeDeployment({ id: 'dep-abc', name: '' });
mockApiResponse([dep]);
@@ -61,6 +61,21 @@ describe('K8sNamespacesDrawer', () => {
expect(navigateMock).toHaveBeenCalledWith('/kubernetes/pods');
});
it('uses the shared platform table spacing and divider contract', async () => {
apiFetchJSONMock.mockResolvedValueOnce(makeResponse([{ namespace: 'default' }]));
const { container } = render(() => <K8sNamespacesDrawer cluster="cluster-a" />);
await waitFor(() => {
expect(screen.getByText('default')).toBeInTheDocument();
});
expect(container.querySelector('table')).toHaveClass('table-fixed');
expect(container.querySelector('thead tr')).toHaveClass('bg-surface-alt');
expect(container.querySelector('tbody')).toHaveClass('divide-y', 'divide-border');
expect(container.querySelector('tbody')).not.toHaveClass('divide-border-subtle');
expect(container.querySelector('th')).toHaveClass('px-1.5', 'py-0.5');
});
it('does not navigate when cluster is empty', async () => {
render(() => <K8sNamespacesDrawer cluster="" />);