Normalize Docker swarm drawer table

This commit is contained in:
rcourtman
2026-05-20 13:02:49 +01:00
parent 4332129ac2
commit daa155c2d2
2 changed files with 82 additions and 26 deletions
@@ -15,6 +15,12 @@ import {
import { EmptyState } from '@/components/shared/EmptyState';
import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
import {
PLATFORM_TABLE_BODY_CLASS,
PLATFORM_TABLE_HEADER_ROW_CLASS,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import {
formatSwarmClusterId,
formatSwarmClusterSummary,
@@ -176,7 +182,8 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
const swarm = createMemo(() => props.swarm);
const clusterName = createMemo(
() => asTrimmedString(swarm()?.clusterName) || asTrimmedString(swarm()?.clusterId) || clusterKey(),
() =>
asTrimmedString(swarm()?.clusterName) || asTrimmedString(swarm()?.clusterId) || clusterKey(),
);
const clusterId = createMemo(() => asTrimmedString(swarm()?.clusterId) ?? '');
@@ -258,36 +265,40 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
}
>
<Card padding="none" tone="card" class="overflow-hidden">
<Table class="w-full min-w-[900px] 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-[960px]">
<TableHeader>
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[18%]`}>
{drawerPresentation.serviceColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[13%]`}>
{drawerPresentation.stackColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[22%]`}>
{drawerPresentation.imageColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[10%]`}>
{drawerPresentation.modeColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}
>
{drawerPresentation.desiredColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}
>
{drawerPresentation.runningColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[12%]`}>
{drawerPresentation.updateColumnLabel}
</TableHead>
<TableHead class="px-3 py-2 font-medium">
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[9%]`}>
{drawerPresentation.portsColumnLabel}
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border-subtle">
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
<For each={filteredServices()}>
{(svc) => {
const name = () => asTrimmedString(svc.name) || svc.id;
@@ -301,8 +312,8 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
const status = () => getSimpleStatusIndicator(svc.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"
@@ -315,18 +326,48 @@ export const SwarmServicesDrawer: Component<{ cluster: string; swarm?: SwarmInfo
</span>
</div>
</TableCell>
<TableCell class="px-3 py-2 text-base-content">{stack()}</TableCell>
<TableCell class="px-3 py-2 text-base-content truncate" title={image()}>
{image()}
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span class="block truncate" title={stack()}>
{stack()}
</span>
</TableCell>
<TableCell class="px-3 py-2 text-base-content">{mode()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{desired()}</TableCell>
<TableCell class="px-3 py-2 text-base-content">{running()}</TableCell>
<TableCell class="px-3 py-2 text-base-content truncate" title={update()}>
{update()}
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span class="block truncate" title={image()}>
{image()}
</span>
</TableCell>
<TableCell class="px-3 py-2 text-base-content truncate" title={ports()}>
{ports()}
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
{mode()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<span class="tabular-nums">{desired()}</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
<span class="tabular-nums">{running()}</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span class="block truncate" title={update()}>
{update()}
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
>
<span class="block truncate font-mono text-[11px]" title={ports()}>
{ports()}
</span>
</TableCell>
</TableRow>
);
@@ -27,7 +27,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>,
@@ -299,6 +299,21 @@ describe('SwarmServicesDrawer', () => {
expect(screen.getByText('2')).toBeInTheDocument();
});
it('uses the shared platform table spacing and divider contract', async () => {
mockApiResponse([makeService({ id: 'svc-1', name: 'web-frontend' })]);
const { container } = render(() => <SwarmServicesDrawer 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 service id as name when name is empty', async () => {
const svc = makeService({ id: 'svc-abc', name: '' });
mockApiResponse([svc]);