Establish canonical platform table column alignment

Until now, the same column (CPU/Memory/Disk) was aligned differently
depending on which table it appeared in: Proxmox top tables defaulted
to left, Docker/TrueNAS/vSphere/Kubernetes top tables used right, the
shared workloads bottom table used center, and integer-count columns
(VMs/CTs/Containers/Datastores/Desired/etc.) were a mix of right and
center. The user's read: "complete hobbledy codge."

Codify the rule once and for all. The new
frontend-modern/src/features/platformPage/columnAlignment.ts defines
five canonical column kinds with a documented align mapping and the
rationale for each:

  - 'name'           left   primary identifier
  - 'text'           left   readable strings
  - 'metric-bar'     center bar fills cell, label rides at fill edge
  - 'numeric-value'  right  units/digits line up for column scanning
  - 'badge'          center pill/icon-only cells

sharedPlatformPage exposes getPlatformTableHeadClassForKind() and
getPlatformTableCellClassForKind() wrappers; every platform top table
plus the Docker services table now consume these instead of passing
literal align strings. The Proxmox host column model carries a kind
field instead of a raw align so the same source of truth drives both
the model file and the JSX.

KubernetesClustersTable.tsx is intentionally not migrated in this
commit: another agent has it staged in their working tree. The same
migration applies once that lands; left for a follow-up.

The WorkloadTableHeader (shared bottom guest-list table) is also
unmigrated here. It center-aligns everything today, and changing it to
canonical means flipping Uptime / Net I/O / Disk I/O from center to
right across every guest list on every platform page — bigger visual
blast radius, so it gets its own scoped commit next.

A canonical-platform-audit rule and a CLAUDE.md/AGENTS.md note will
follow in a third commit, so any future agent that writes a
non-canonical align fails the pre-push hook rather than discovers the
drift visually weeks later.
This commit is contained in:
rcourtman
2026-05-20 11:06:32 +01:00
parent 881e1ffda6
commit 348b731408
10 changed files with 239 additions and 142 deletions
@@ -27,8 +27,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Disk } from '@/types/api';
@@ -166,36 +166,36 @@ export const DockerHostsTable: Component<{
bars aren't squeezed by table-fixed's equal split. Mobile
widths (w-[40%], w-[20%]) are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClass()} w-[40%] md:w-[15%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} w-[40%] md:w-[15%]`}>
Host
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[7%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[7%]`}>
System
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[7%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[7%]`}>
Version
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[9%]`}>
Containers
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} w-[20%] md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} w-[20%] md:w-[14%]`}>
CPU
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} w-[20%] md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} w-[20%] md:w-[14%]`}>
<span class="md:hidden">Mem</span>
<span class="hidden md:inline">Memory</span>
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} w-[20%] md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} w-[20%] md:w-[14%]`}>
Disk
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[6%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
Uptime
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[6%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
Temp
</TableHead>
<Show when={showSwarmColumn()}>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[9%]`}>
Swarm role
</TableHead>
</Show>
@@ -262,7 +262,7 @@ export const DockerHostsTable: Component<{
onKeyDown={handleActivationKey}
tabIndex={0}
>
<TableCell class={`${getPlatformTableCellClass()} w-[40%] md:w-auto`}>
<TableCell class={`${getPlatformTableCellClassForKind('name')} w-[40%] md:w-auto`}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -286,7 +286,7 @@ export const DockerHostsTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
<Show when={systemBadge()} fallback={<span class="text-muted">—</span>}>
{(badge) => (
@@ -300,17 +300,17 @@ export const DockerHostsTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden font-mono text-[11px] text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
{version()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{containerCount()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<ResponsiveMetricCell
class="w-full"
@@ -322,7 +322,7 @@ export const DockerHostsTable: Component<{
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<Show
when={canRenderMetrics() && hasMemoryMetric()}
@@ -336,7 +336,7 @@ export const DockerHostsTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<Show
when={canRenderMetrics() && hasDiskMetric()}
@@ -350,18 +350,18 @@ export const DockerHostsTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{formatUptime(host.uptime ?? docker()?.uptimeSeconds)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{formatTemperature(host.temperature ?? docker()?.temperature)}
</TableCell>
<Show when={showSwarmColumn()}>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{swarmRole()}
</TableCell>
@@ -21,8 +21,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
@@ -135,23 +135,23 @@ export const DockerServicesTable: Component<{
a middle slice for port lists and hostnames. Mobile
widths are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClass()} md:w-[22%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[22%]`}>
Service
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[25%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[25%]`}>
Image
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} md:w-[9%]`}>Mode</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[8%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} md:w-[9%]`}>Mode</TableHead>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[8%]`}>
Desired
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[8%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[8%]`}>
Running
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}>
Ports
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}>
Host
</TableHead>
</TableRow>
@@ -166,7 +166,7 @@ export const DockerServicesTable: Component<{
const indicator = () => getSimpleStatusIndicator(service.status);
return (
<TableRow class="text-[11px] sm:text-xs">
<TableCell class={getPlatformTableCellClass()}>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -180,34 +180,34 @@ export const DockerServicesTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
<span class="truncate inline-block max-w-[18rem]" title={image()}>
{image()}
</span>
</TableCell>
<TableCell class={`${getPlatformTableCellClass()} text-base-content`}>
<TableCell class={`${getPlatformTableCellClassForKind('text')} text-base-content`}>
{mode()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{replicaCount(service.docker?.desiredTasks)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
{replicaCount(service.docker?.runningTasks)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
<span class="font-mono text-[11px]" title={formatPorts(service.docker)}>
{formatPorts(service.docker)}
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{host()}
</TableCell>
@@ -21,8 +21,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
@@ -101,25 +101,25 @@ export const KubernetesDeploymentsTable: Component<{
Ready / Available) trim to what their headers plus
1-2 digit values need. Mobile widths are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClass()} md:w-[27%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[27%]`}>
Deployment
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[21%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[21%]`}>
Namespace
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[16%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[16%]`}>
Cluster
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[9%]`}>
Desired
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[9%]`}>
Updated
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[9%]`}>
Ready
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[9%]`}>
Available
</TableHead>
</TableRow>
@@ -136,7 +136,7 @@ export const KubernetesDeploymentsTable: Component<{
const indicator = () => getSimpleStatusIndicator(deployment.status);
return (
<TableRow class="text-[11px] sm:text-xs">
<TableCell class={getPlatformTableCellClass()}>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -150,32 +150,32 @@ export const KubernetesDeploymentsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{ns()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{cluster()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{replicaCount(deployment.kubernetes?.desiredReplicas)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{replicaCount(deployment.kubernetes?.updatedReplicas)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
{replicaCount(deployment.kubernetes?.readyReplicas)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
{replicaCount(deployment.kubernetes?.availableReplicas)}
</TableCell>
@@ -24,8 +24,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
@@ -152,31 +152,31 @@ export const KubernetesNodesTable: Component<{
Roles, Kubelet, Uptime) trim accordingly. Mobile
widths are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClass()} md:w-[15%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[15%]`}>
Node
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[10%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}>
Cluster
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[10%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}>
Roles
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[8%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[8%]`}>
Kubelet
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[15%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[15%]`}>
Runtime
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[11%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
CPU
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[11%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
Memory
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[14%]`}>
Capacity
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[6%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
Uptime
</TableHead>
</TableRow>
@@ -228,7 +228,7 @@ export const KubernetesNodesTable: Component<{
onKeyDown={drawer.handleActivationKey(node)}
tabIndex={0}
>
<TableCell class={getPlatformTableCellClass()}>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -242,29 +242,29 @@ export const KubernetesNodesTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{cluster()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{formatRoles(meta()?.roles)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden font-mono text-[11px] text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
{kubelet()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden font-mono text-[11px] text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
<span class="truncate inline-block max-w-[10rem]" title={runtime()}>
{runtime()}
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<ResponsiveMetricCell
class="w-full"
@@ -276,7 +276,7 @@ export const KubernetesNodesTable: Component<{
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<Show
when={canRenderMetrics() && hasMemoryMetric()}
@@ -290,13 +290,13 @@ export const KubernetesNodesTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content tabular-nums`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
<span class="md:hidden">{compactCapacityLabel()}</span>
<span class="hidden md:inline">{capacityLabel()}</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{formatUptime(node.uptime)}
</TableCell>
@@ -0,0 +1,81 @@
import type { PlatformTableCellAlign } from './sharedPlatformPage';
/**
* Canonical column kinds for platform tables.
*
* Every table that lives on a platform overview page (Proxmox / Docker /
* Kubernetes / TrueNAS / vSphere top tables, the shared workloads bottom
* table, services tables, deployments tables, and the like) should pick
* a kind for each column from this enum. The kind drives header and cell
* alignment via `PLATFORM_COLUMN_ALIGN_BY_KIND` so the same column type
* lines up the same way everywhere.
*
* Pick the kind that matches what the cell *renders*:
*
* - 'name' Primary identifier (bold, usually the first column).
* Host, Node, System, Service, Deployment.
*
* - 'text' Readable string content (single or multi-word).
* Version, Datacenter, Cluster, vCenter, Runtime,
* Roles, Kubelet, Image, Mode, Power, Swarm role,
* Ports, Capacity-as-text.
*
* - 'metric-bar' A horizontal usage bar that fills the cell width,
* with the percentage / size label riding at the
* bar's fill edge.
* CPU, Memory, Disk, Storage.
*
* - 'numeric-value' Scalar text with units, or pure digits, where
* column-scanning the values matters.
* Uptime, Temp, integer counts (VMs, CTs,
* Containers, Pods, Pools, Datasets, Datastores,
* Disks, Apps, Desired, Updated, Ready, Available),
* Net I/O, Disk I/O.
*
* - 'badge' A pill or icon-only cell with no scannable value.
* Backup, Tags, Update.
*
* Rationale for the mapping:
*
* - 'metric-bar' is centered because the bar fills the cell and the
* percentage label slides along the fill edge depending on the
* value; the header sits over the column's visual center, where
* the eye naturally goes, instead of floating at a left or right
* edge that doesn't correspond to anything in the cell.
*
* - 'numeric-value' is right-aligned so units ("d", "h", "GB",
* "MB/s") and digit endings line up vertically for fast column
* scanning (which host is highest? which has shortest uptime?).
*
* - 'name' and 'text' are left-aligned for reading.
*
* - 'badge' is centered because the rendered content is a square-ish
* pill or icon, not a scannable value.
*
* This file is the single source of truth. To change the canonical, edit
* the map below; every table that consumes these helpers picks up the
* change automatically. `scripts/canonical-platform-audit.mjs` enforces
* that platform tables use canonical alignments for well-known column
* labels, so drift breaks pre-push.
*/
export type PlatformTableColumnKind =
| 'name'
| 'text'
| 'metric-bar'
| 'numeric-value'
| 'badge';
export const PLATFORM_COLUMN_ALIGN_BY_KIND: Record<
PlatformTableColumnKind,
PlatformTableCellAlign
> = {
name: 'left',
text: 'left',
'metric-bar': 'center',
'numeric-value': 'right',
badge: 'center',
};
export const getPlatformColumnAlign = (
kind: PlatformTableColumnKind,
): PlatformTableCellAlign => PLATFORM_COLUMN_ALIGN_BY_KIND[kind];
@@ -7,6 +7,10 @@ import { SearchInput } from '@/components/shared/SearchInput';
import { TableCard } from '@/components/shared/TableCard';
import { UnifiedResourceTable } from '@/components/Infrastructure/UnifiedResourceTable';
import type { Resource, ResourceStatus } from '@/types/resource';
import {
getPlatformColumnAlign,
type PlatformTableColumnKind,
} from './columnAlignment';
export type PlatformTabSpec<TabId extends string> = {
id: TabId;
@@ -75,6 +79,17 @@ export const getPlatformTableHeadClass = (align?: PlatformTableCellAlign): strin
export const getPlatformTableCellClass = (align?: PlatformTableCellAlign): string =>
`px-1.5 sm:px-2 py-1 ${getPlatformTableAlignClass(align)}`.trim();
// Canonical kind-based wrappers. Tables should consume these instead of
// passing literal align strings, so every CPU/Memory/Disk/Storage header
// in the app lines up the same way (and any future column type can be
// added once in columnAlignment.ts and propagated automatically). See
// PlatformTableColumnKind for the kind list and rationale.
export const getPlatformTableHeadClassForKind = (kind: PlatformTableColumnKind): string =>
getPlatformTableHeadClass(getPlatformColumnAlign(kind));
export const getPlatformTableCellClassForKind = (kind: PlatformTableColumnKind): string =>
getPlatformTableCellClass(getPlatformColumnAlign(kind));
export function PlatformErrorState(props: {
title: string;
description: string;
@@ -35,8 +35,8 @@ import {
PLATFORM_TABLE_CARD_CLASS,
PLATFORM_TABLE_HEADER_ROW_CLASS,
PlatformTableEmptyState,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
} from '@/features/platformPage/sharedPlatformPage';
import { type WorkloadsMetricDisplayMode } from '@/components/Workloads/workloadsFilterModel';
import { type WorkloadTableMetricHistoryRange } from '@/components/Workloads/workloadMetricHistoryModel';
@@ -183,7 +183,7 @@ export const ProxmoxNodesTable: Component<{
<TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}>
<For each={visibleColumns()}>
{(column) => (
<TableHead class={getPlatformTableHeadClass(column.align)}>
<TableHead class={getPlatformTableHeadClassForKind(column.kind)}>
{column.label}
</TableHead>
)}
@@ -246,7 +246,7 @@ export const ProxmoxNodesTable: Component<{
switch (column.id) {
case 'node':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -262,7 +262,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'version':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<Show when={version()} fallback={<span class="text-muted">—</span>}>
<span class="inline-flex items-center rounded bg-surface-alt px-1.5 py-0.5 font-mono text-[10px] text-base-content">
{version()}
@@ -273,7 +273,7 @@ export const ProxmoxNodesTable: Component<{
case 'uptime':
return (
<TableCell
class={`${getPlatformTableCellClass(column.align)} tabular-nums ${
class={`${getPlatformTableCellClassForKind(column.kind)} tabular-nums ${
uptime().warn
? 'text-orange-600 dark:text-orange-400'
: 'text-base-content'
@@ -284,7 +284,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'cpu':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<Show
when={isSparklineMode()}
fallback={
@@ -308,7 +308,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'memory':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<Show
when={isSparklineMode()}
fallback={
@@ -342,7 +342,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'disk':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<Show
when={isSparklineMode()}
fallback={
@@ -378,7 +378,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'temp':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<Show
when={
typeof temperature() === 'number' && (temperature() as number) > 0
@@ -391,7 +391,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'vms':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<span class={counts().vms > 0 ? VMS_BADGE : ZERO_BADGE}>
{counts().vms}
</span>
@@ -399,7 +399,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'cts':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<span class={counts().containers > 0 ? CTS_BADGE : ZERO_BADGE}>
{counts().containers}
</span>
@@ -407,7 +407,7 @@ export const ProxmoxNodesTable: Component<{
);
case 'cluster':
return (
<TableCell class={getPlatformTableCellClass(column.align)}>
<TableCell class={getPlatformTableCellClassForKind(column.kind)}>
<span class="inline-flex items-center rounded-md bg-surface-alt px-2 py-0.5 text-[11px] font-medium text-base-content">
{cluster()}
</span>
@@ -1,6 +1,7 @@
import type { JSX } from 'solid-js';
import type { WorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel';
import type { PlatformTableColumnKind } from '@/features/platformPage/columnAlignment';
export type ProxmoxHostTableColumnId =
| 'node'
@@ -17,7 +18,7 @@ export type ProxmoxHostTableColumnId =
export type ProxmoxHostTableColumn = {
id: ProxmoxHostTableColumnId;
label: string;
align?: 'left' | 'right' | 'center';
kind: PlatformTableColumnKind;
};
const HOST_TABLE_LAYOUT_ORDER: Record<WorkloadTableLayoutMode, number> = {
@@ -84,16 +85,16 @@ const HOST_COLUMN_RESPONSIVE_WEIGHTS: Record<
const formatPercentage = (value: number): string => `${Number(value.toFixed(4))}%`;
export const PROXMOX_HOST_TABLE_COLUMNS: ProxmoxHostTableColumn[] = [
{ id: 'node', label: 'Node' },
{ id: 'version', label: 'Version' },
{ id: 'uptime', label: 'Uptime', align: 'right' },
{ id: 'cpu', label: 'CPU' },
{ id: 'memory', label: 'Memory' },
{ id: 'disk', label: 'Disk' },
{ id: 'temp', label: 'Temp', align: 'right' },
{ id: 'vms', label: 'VMs', align: 'center' },
{ id: 'cts', label: 'CTs', align: 'center' },
{ id: 'cluster', label: 'Cluster' },
{ id: 'node', label: 'Node', kind: 'name' },
{ id: 'version', label: 'Version', kind: 'text' },
{ id: 'uptime', label: 'Uptime', kind: 'numeric-value' },
{ id: 'cpu', label: 'CPU', kind: 'metric-bar' },
{ id: 'memory', label: 'Memory', kind: 'metric-bar' },
{ id: 'disk', label: 'Disk', kind: 'metric-bar' },
{ id: 'temp', label: 'Temp', kind: 'numeric-value' },
{ id: 'vms', label: 'VMs', kind: 'numeric-value' },
{ id: 'cts', label: 'CTs', kind: 'numeric-value' },
{ id: 'cluster', label: 'Cluster', kind: 'text' },
];
export const getProxmoxHostVisibleColumnsForLayout = (
@@ -24,8 +24,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
@@ -173,37 +173,37 @@ export const TrueNASSystemsTable: Component<{
room it needs for full "TrueNAS-SCALE-24.10.2"-style
labels. Mobile widths are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClass()} md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[14%]`}>
System
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[14%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}>
Version
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[6%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
Uptime
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[11%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[11%]`}>
CPU
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[12%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[12%]`}>
Memory
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[13%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[13%]`}>
Storage
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[6%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
Temp
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[6%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[6%]`}>
Pools
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[7%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[7%]`}>
Datasets
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[5%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}>
Disks
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[5%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[5%]`}>
Apps
</TableHead>
</TableRow>
@@ -253,7 +253,7 @@ export const TrueNASSystemsTable: Component<{
onKeyDown={drawer.handleActivationKey(system)}
tabIndex={0}
>
<TableCell class={getPlatformTableCellClass()}>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -267,17 +267,17 @@ export const TrueNASSystemsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden font-mono text-[11px] text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
{version()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{formatUptime(system.uptime)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<ResponsiveMetricCell
class="w-full"
@@ -289,7 +289,7 @@ export const TrueNASSystemsTable: Component<{
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<Show
when={canRenderMetrics() && hasMemoryMetric()}
@@ -303,33 +303,33 @@ export const TrueNASSystemsTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content`}
class={`${getPlatformTableCellClassForKind('metric-bar')} text-base-content`}
>
<span class="md:hidden">{formatPercent(storagePercent())}</span>
<span class="hidden md:inline">{storageFullLabel()}</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
>
{formatTemperature(system.temperature)}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c.pools}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c.datasets}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c.disks}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{c.apps}
</TableCell>
@@ -24,8 +24,8 @@ import {
PlatformTableEmptyState,
createPlatformTableFilterState,
filterPlatformResources,
getPlatformTableCellClass,
getPlatformTableHeadClass,
getPlatformTableCellClassForKind,
getPlatformTableHeadClassForKind,
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import {
@@ -156,31 +156,31 @@ export const VsphereHostsTable: Component<{
integer-count columns to what their content actually
needs. Mobile widths are unchanged.
*/}
<TableHead class={`${getPlatformTableHeadClass()} md:w-[18%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[18%]`}>
Host
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[11%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[11%]`}>
Datacenter
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[12%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[12%]`}>
Cluster
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[9%]`}>
Power
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[13%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[13%]`}>
CPU
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[13%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('metric-bar')} md:w-[13%]`}>
Memory
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} hidden md:table-cell md:w-[9%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[9%]`}>
Datastores
</TableHead>
<TableHead class={`${getPlatformTableHeadClass('right')} md:w-[5%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[5%]`}>
VMs
</TableHead>
<TableHead class={`${getPlatformTableHeadClass()} hidden md:table-cell md:w-[10%]`}>
<TableHead class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[10%]`}>
vCenter
</TableHead>
</TableRow>
@@ -220,7 +220,7 @@ export const VsphereHostsTable: Component<{
onKeyDown={drawer.handleActivationKey(host)}
tabIndex={0}
>
<TableCell class={getPlatformTableCellClass()}>
<TableCell class={getPlatformTableCellClassForKind('name')}>
<div class="flex min-w-0 items-center gap-2">
<StatusDot
size="sm"
@@ -234,16 +234,16 @@ export const VsphereHostsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{datacenter()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
>
{cluster()}
</TableCell>
<TableCell class={`${getPlatformTableCellClass()} hidden md:table-cell`}>
<TableCell class={`${getPlatformTableCellClassForKind('text')} hidden md:table-cell`}>
<div class="flex items-center gap-2">
<StatusDot
size="sm"
@@ -257,7 +257,7 @@ export const VsphereHostsTable: Component<{
</div>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<ResponsiveMetricCell
class="w-full"
@@ -269,7 +269,7 @@ export const VsphereHostsTable: Component<{
/>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} w-[20%] md:w-auto`}
class={`${getPlatformTableCellClassForKind('metric-bar')} w-[20%] md:w-auto`}
>
<Show
when={canRenderMetrics() && hasMemoryMetric()}
@@ -283,17 +283,17 @@ export const VsphereHostsTable: Component<{
</Show>
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} hidden text-base-content tabular-nums md:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums md:table-cell`}
>
{datastoreCount()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass('right')} text-base-content tabular-nums`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{vmCount()}
</TableCell>
<TableCell
class={`${getPlatformTableCellClass()} hidden font-mono text-[11px] text-base-content md:table-cell`}
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
>
<span class="inline-block max-w-[12rem] truncate" title={vcenter()}>
{vcenter()}