fix(ui): prioritize responsive table columns

Contract-Neutral: Responsive table layout only; no API or contract changes.
This commit is contained in:
rcourtman
2026-08-05 13:17:06 +01:00
parent 7d066f1d5c
commit ce91bcb53e
6 changed files with 275 additions and 47 deletions
+51 -19
View File
@@ -1,45 +1,77 @@
{
"version": 1,
"base_sha": "58864f5ecc3abd351cf252b07995d605e3f7c498",
"verified_at": "2026-08-05T11:30:51Z",
"base_sha": "7d066f1d5c2f8921695cae5cf5bddf2e2b8e7151",
"verified_at": "2026-08-05T12:07:34Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx",
"frontend-modern/src/features/truenas/TrueNASAppsTable.tsx",
"frontend-modern/src/index.css"
"frontend-modern/src/features/docker/DockerNetworksTable.tsx",
"frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx",
"frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx",
"frontend-modern/src/features/vmware/VsphereNetworksTable.tsx"
],
"content_sha256": {
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx": "c614aee58b3953fdafb6e65035c73796dcc5ae3b6b8a55479632e61d95af1f12",
"frontend-modern/src/features/truenas/TrueNASAppsTable.tsx": "50374a199a49dcac90386a1b22cc45c38674225defba469bad3d1d263ae6b8df",
"frontend-modern/src/index.css": "817c3f407d83909e43bb771019f0b7b460b498d0333e9eb3cac2fe697b5d78be"
"frontend-modern/src/features/docker/DockerNetworksTable.tsx": "a5a10d77fc0f575d05fb5754d7b65840692c9fde7a726899f1a4814c2b5cd2b1",
"frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "9571289310a518495c88860089cd0ba191c69cd4821c7ba90163e97a42217c12",
"frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "1ec149f222086b4e9433d07b15699775cb6eca5a20ee968eaf817f030f4f3ac1",
"frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "7c2580565602b9e7927ea4a16548f104d0144ec37ec93f692cc882171b1f9b57"
},
"routes": [
"/proxmox/overview",
"/proxmox/storage",
"/proxmox/replication",
"/proxmox/backups",
"/proxmox/ceph",
"/proxmox/mail",
"/docker/overview",
"/docker/images",
"/docker/storage",
"/docker/networks",
"/docker/swarm",
"/kubernetes/overview",
"/kubernetes/nodes",
"/kubernetes/workloads",
"/kubernetes/services",
"/kubernetes/storage",
"/kubernetes/configuration",
"/kubernetes/events",
"/truenas/overview",
"/truenas/storage",
"/truenas/services",
"/truenas/apps",
"/truenas/vms",
"/truenas/shares",
"/vmware/overview"
"/truenas/protection",
"/vmware/overview",
"/vmware/storage",
"/vmware/networks",
"/vmware/health",
"/vmware/activity",
"/standalone/machines",
"/standalone/availability"
],
"viewports": [
{ "width": 320, "height": 900 },
{ "width": 375, "height": 812 },
{ "width": 375, "height": 900 },
{ "width": 768, "height": 900 },
{ "width": 1024, "height": 900 },
{ "width": 1280, "height": 900 },
{ "width": 1600, "height": 900 }
],
"states": [
"Rendered TrueNAS Apps with App, CPU, Memory, and Updates using the complete phone-width table instead of reserving hidden desktop tracks",
"Kept expanded TrueNAS App and Share detail rows aligned as visible column counts changed from phone through desktop breakpoints",
"Kept expanded Kubernetes deployment, Docker host, vSphere host, Proxmox node, and Proxmox workload rows aligned to their visible summary columns",
"Measured visible header widths against table widths at 320, 375, 768, 1024, 1280, and 1600 pixels with no document overflow",
"Visually inspected the expanded Proxmox node and workload details at phone width with operational columns and drawer content remaining readable"
"Rendered every audited platform table at phone, tablet, laptop, desktop, and wide-desktop widths without document overflow",
"Kept Proxmox replication guest names readable by prioritizing Guest over Job, Fails, and Error while progressively revealing operational columns",
"Prioritized Docker network attachment summaries over the compact Attention field and retained useful network, subnet, driver, and host context as space increased",
"Displayed TrueNAS VM state, CPU, and memory at phone width while deferring secondary Flags metadata until tablet width",
"Displayed vSphere network VM impact at phone width alongside network name and type, with hosts and datacenter returning at wider layouts",
"Kept representative expanded detail rows aligned to the actual visible summary columns after resizing in both directions",
"Matched Proxmox, Docker, Kubernetes, TrueNAS, vSphere, and Machines page, tab, panel, and table bounds at 1280 pixels"
],
"interactions": [
"Expanded and collapsed representative resource rows using their disclosure controls across Proxmox, Docker, Kubernetes, TrueNAS, and vSphere",
"Resized the live browser repeatedly across every audited breakpoint while each representative detail row remained open",
"Sorted TrueNAS Apps by CPU through descending and ascending states, then restored the default order",
"Verified responsive detail colspans returned to compact values after resizing from desktop back to phone width"
"Expanded representative healthy, degraded, stopped, pending, warning, and failed rows across Proxmox, Docker, Kubernetes, TrueNAS, vSphere, and Machines",
"Resized each audited table repeatedly through 320, 375, 768, 1024, 1280, and 1600 pixel widths while representative details remained open",
"Switched Proxmox backup views and storage tabs to exercise their independent table layouts",
"Verified final TrueNAS VM detail colspans changed from 4 compact columns to 7 desktop columns",
"Verified final vSphere network detail colspans changed from 3 compact columns to 5 expanded columns",
"Measured every visible header allocation against its table width and checked for horizontal document overflow"
]
}
@@ -12,6 +12,7 @@ import {
getPlatformTableCellClassForKind,
getPlatformTableContainerLayout,
getPlatformTableHeadClassForKind,
type PlatformTableContainerLayout,
PlatformTableShell,
} from '@/features/platformPage/sharedPlatformPage';
import { useObservedElementWidth } from '@/hooks/useObservedElementWidth';
@@ -46,6 +47,54 @@ type AttachmentGroupKey = 'attention' | 'running' | 'other';
type AttachmentStatusFilter = 'all' | AttachmentGroupKey;
type DockerNetworkColumn = 'network' | 'attached' | 'attention' | 'subnet' | 'driver' | 'host';
const DOCKER_NETWORK_COLUMN_WIDTH_CLASS: Record<
PlatformTableContainerLayout,
Record<DockerNetworkColumn, string>
> = {
compact: {
network: 'w-[40%]',
attached: 'w-[40%]',
attention: 'w-[20%]',
subnet: 'w-0',
driver: 'w-0',
host: 'w-0',
},
basic: {
network: 'w-[38%]',
attached: 'w-[42%]',
attention: 'w-[20%]',
subnet: 'w-0',
driver: 'w-0',
host: 'w-0',
},
operational: {
network: 'w-[25%]',
attached: 'w-[35%]',
attention: 'w-[15%]',
subnet: 'w-[25%]',
driver: 'w-0',
host: 'w-0',
},
expanded: {
network: 'w-[23%]',
attached: 'w-[32%]',
attention: 'w-[13%]',
subnet: 'w-[22%]',
driver: 'w-[10%]',
host: 'w-0',
},
full: {
network: 'w-[20%]',
attached: 'w-[27%]',
attention: 'w-[13%]',
subnet: 'w-[18%]',
driver: 'w-[10%]',
host: 'w-[12%]',
},
};
type AttachmentGroup = {
key: AttachmentGroupKey;
label: string;
@@ -369,6 +418,8 @@ export const DockerNetworksTable: Component<DockerNetworksTableProps> = (props)
const showSubnet = createMemo(() => !['compact', 'basic'].includes(layout()));
const showDriver = createMemo(() => ['expanded', 'full'].includes(layout()));
const showHost = createMemo(() => layout() === 'full');
const columnWidthClass = (column: DockerNetworkColumn) =>
DOCKER_NETWORK_COLUMN_WIDTH_CLASS[layout()][column];
const visibleColumnCount = createMemo(
() => 3 + Number(showSubnet()) + Number(showDriver()) + Number(showHost()),
);
@@ -414,19 +465,41 @@ export const DockerNetworksTable: Component<DockerNetworksTableProps> = (props)
tableClass="min-w-[0px] table-fixed text-xs"
header={
<>
<TableHead class={getPlatformTableHeadClassForKind('name')}>Network</TableHead>
<TableHead class={getPlatformTableHeadClassForKind('text')}>
<TableHead
class={`${getPlatformTableHeadClassForKind('name')} ${columnWidthClass('network')}`}
>
Network
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('attached')}`}
>
<PlatformResponsiveTableLabel compact="Attached" full="Attached workloads" />
</TableHead>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Attention</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('attention')}`}
>
Attention
</TableHead>
<Show when={showSubnet()}>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Subnets</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('subnet')}`}
>
Subnets
</TableHead>
</Show>
<Show when={showDriver()}>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Driver</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('driver')}`}
>
Driver
</TableHead>
</Show>
<Show when={showHost()}>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Host</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('host')}`}
>
Host
</TableHead>
</Show>
</>
}
@@ -301,6 +301,9 @@ describe('platform overview layout guardrails', () => {
expect(dockerPageSurfaceSource).toContain('<DockerImagesTable');
expect(dockerPageSurfaceSource).toContain('<DockerVolumesTable');
expect(dockerPageSurfaceSource).toContain('<DockerNetworksTable');
expect(dockerNetworksTableSource).toContain('DOCKER_NETWORK_COLUMN_WIDTH_CLASS');
expect(dockerNetworksTableSource).toContain("attached: 'w-[35%]'");
expect(dockerNetworksTableSource).toContain("attention: 'w-[15%]'");
expect(dockerPageSurfaceSource).not.toContain('<WorkloadsSurface');
expect(dockerPageSurfaceSource).toContain('<DockerSwarmNodesTable');
expect(dockerPageSurfaceSource).toContain('<DockerServicesTable');
@@ -345,6 +348,9 @@ describe('platform overview layout guardrails', () => {
expect(truenasProtectionTableSource).toContain('md:min-w-[960px]');
expect(truenasStorageTopologyTableSource).toContain('md:min-w-[960px]');
expect(truenasVirtualMachinesTableSource).toContain('md:min-w-[960px]');
expect(truenasVirtualMachinesTableSource).toMatch(
/class="hidden sm:table-cell md:w-\[19%\]"[\s\S]{0,120}?Flags/,
);
expect(vmwarePageSurfaceSource).toContain('<VsphereHostsTable');
expect(vmwarePageSurfaceSource).toContain('<VsphereAlertsTable');
expect(vmwarePageSurfaceSource).toContain("activeTab() === 'health'");
@@ -355,10 +361,16 @@ describe('platform overview layout guardrails', () => {
expect(vmwarePageSurfaceSource).toContain('suppressTypeFilter');
expect(vmwarePageSurfaceSource).toContain('<VsphereDatastoresTable');
expect(vmwarePageSurfaceSource).toContain('<VsphereNetworksTable');
expect(vsphereNetworksTableSource).toMatch(
/sortKey="vms"[\s\S]{0,120}?class="w-\[20%\] md:w-\[7%\]"/,
);
expect(vmwarePageSurfaceSource).toContain('<VsphereActivityTable');
expect(vmwarePageSurfaceSource).not.toContain('<StorageSurface');
expect(vmwarePageSurfaceSource).not.toContain('forcedView="pools"');
expect(proxmoxPageSurfaceSource).toContain('suppressNodeFilter');
expect(proxmoxReplicationTableSource).toContain('REPLICATION_COLUMN_WIDTH_CLASS');
expect(proxmoxReplicationTableSource).toContain("guest: 'w-[20%]'");
expect(proxmoxReplicationTableSource).toContain("fails: 'w-[4%]'");
expect(standalonePageSurfaceSource).toContain('<AgentsMachinesTable');
expect(standalonePageSurfaceSource).not.toContain('InfrastructureSummary');
expect(standalonePageSurfaceSource).not.toContain('StickySummarySection');
@@ -13,6 +13,7 @@ import {
getPlatformTableCellClassForKind,
getPlatformTableContainerLayout,
getPlatformTableHeadClassForKind,
type PlatformTableContainerLayout,
type PlatformTableFilterOption,
PlatformTableEmptyState,
PlatformTableLoadingState,
@@ -30,6 +31,84 @@ import type { ReplicationJob, ReplicationJobsResponse } from '@/types/api';
type ReplicationStatusFilter = 'all' | 'healthy' | 'failed' | 'pending' | 'disabled';
type ReplicationColumn =
| 'status'
| 'job'
| 'guest'
| 'route'
| 'schedule'
| 'lastSync'
| 'nextSync'
| 'duration'
| 'fails'
| 'error';
const REPLICATION_COLUMN_WIDTH_CLASS: Record<
PlatformTableContainerLayout,
Record<ReplicationColumn, string>
> = {
compact: {
status: 'w-[20%]',
job: 'w-0',
guest: 'w-[40%]',
route: 'w-[20%]',
schedule: 'w-0',
lastSync: 'w-[20%]',
nextSync: 'w-0',
duration: 'w-0',
fails: 'w-0',
error: 'w-0',
},
basic: {
status: 'w-[14%]',
job: 'w-[11%]',
guest: 'w-[28%]',
route: 'w-[17%]',
schedule: 'w-0',
lastSync: 'w-[15%]',
nextSync: 'w-[15%]',
duration: 'w-0',
fails: 'w-0',
error: 'w-0',
},
operational: {
status: 'w-[11%]',
job: 'w-[7%]',
guest: 'w-[21%]',
route: 'w-[12%]',
schedule: 'w-[9%]',
lastSync: 'w-[11%]',
nextSync: 'w-[13%]',
duration: 'w-[11%]',
fails: 'w-[5%]',
error: 'w-0',
},
expanded: {
status: 'w-[10%]',
job: 'w-[7%]',
guest: 'w-[20%]',
route: 'w-[11%]',
schedule: 'w-[8%]',
lastSync: 'w-[10%]',
nextSync: 'w-[12%]',
duration: 'w-[10%]',
fails: 'w-[4%]',
error: 'w-[8%]',
},
full: {
status: 'w-[10%]',
job: 'w-[7%]',
guest: 'w-[20%]',
route: 'w-[11%]',
schedule: 'w-[8%]',
lastSync: 'w-[10%]',
nextSync: 'w-[12%]',
duration: 'w-[10%]',
fails: 'w-[4%]',
error: 'w-[8%]',
},
};
const STATUS_FILTER_OPTIONS: PlatformTableFilterOption<ReplicationStatusFilter>[] = [
{ value: 'all', label: 'All' },
{
@@ -193,6 +272,8 @@ export const ProxmoxReplicationTable: Component<{
const showNext = createMemo(() => layout() !== 'compact');
const showOperational = createMemo(() => ['operational', 'expanded', 'full'].includes(layout()));
const showError = createMemo(() => ['expanded', 'full'].includes(layout()));
const columnWidthClass = (column: ReplicationColumn) =>
REPLICATION_COLUMN_WIDTH_CLASS[layout()][column];
return (
<Show
@@ -255,40 +336,66 @@ export const ProxmoxReplicationTable: Component<{
tableClass="min-w-[0px] table-fixed text-xs"
header={
<>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Status</TableHead>
<Show when={showJob()}>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Job</TableHead>
</Show>
<TableHead class={getPlatformTableHeadClassForKind('name')}>Guest</TableHead>
<TableHead
class={getPlatformTableHeadClassForKind('text')}
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('status')}`}
>
Status
</TableHead>
<Show when={showJob()}>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('job')}`}
>
Job
</TableHead>
</Show>
<TableHead
class={`${getPlatformTableHeadClassForKind('name')} ${columnWidthClass('guest')}`}
>
Guest
</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('route')}`}
title="Source → target"
>
Route
</TableHead>
<Show when={showOperational()}>
<TableHead class={getPlatformTableHeadClassForKind('text')}>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('schedule')}`}
>
Schedule
</TableHead>
</Show>
<TableHead class={getPlatformTableHeadClassForKind('numeric-value')}>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} ${columnWidthClass('lastSync')}`}
>
Last sync
</TableHead>
<Show when={showNext()}>
<TableHead class={getPlatformTableHeadClassForKind('numeric-value')}>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} ${columnWidthClass('nextSync')}`}
>
Next sync
</TableHead>
</Show>
<Show when={showOperational()}>
<TableHead class={getPlatformTableHeadClassForKind('numeric-value')}>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} ${columnWidthClass('duration')}`}
>
Duration
</TableHead>
<TableHead class={getPlatformTableHeadClassForKind('numeric-value')}>
<TableHead
class={`${getPlatformTableHeadClassForKind('numeric-value')} ${columnWidthClass('fails')}`}
>
Fails
</TableHead>
</Show>
<Show when={showError()}>
<TableHead class={getPlatformTableHeadClassForKind('text')}>Error</TableHead>
<TableHead
class={`${getPlatformTableHeadClassForKind('text')} ${columnWidthClass('error')}`}
>
Error
</TableHead>
</Show>
</>
}
@@ -219,7 +219,7 @@ export const TrueNASVirtualMachinesTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="cpu"
class="hidden sm:table-cell md:w-[10%]"
class="md:w-[10%]"
>
CPU
</PlatformSortableTableHead>
@@ -227,7 +227,7 @@ export const TrueNASVirtualMachinesTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="memory"
class="hidden sm:table-cell md:w-[10%]"
class="md:w-[10%]"
>
Memory
</PlatformSortableTableHead>
@@ -246,7 +246,11 @@ export const TrueNASVirtualMachinesTable: Component<{
>
Devices
</PlatformSortableTableHead>
<PlatformSortableTableHead kind="text" sort={sort} class="md:w-[19%]">
<PlatformSortableTableHead
kind="text"
sort={sort}
class="hidden sm:table-cell md:w-[19%]"
>
Flags
</PlatformSortableTableHead>
</>
@@ -312,12 +316,12 @@ export const TrueNASVirtualMachinesTable: Component<{
</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content sm:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
{formatCPU(vm())}
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content sm:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
>
{formatPlatformTableBytesValue(vm()?.memoryBytes, '-')}
</TableCell>
@@ -333,7 +337,7 @@ export const TrueNASVirtualMachinesTable: Component<{
<span class="truncate">{devices().label}</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content sm:table-cell`}
title={flags().join(', ')}
>
<Show
@@ -173,7 +173,7 @@ export const VsphereNetworksTable: Component<{
kind="name"
sort={sort}
sortKey="network"
class="md:w-[24%]"
class="w-[45%] md:w-[24%]"
>
Network
</PlatformSortableTableHead>
@@ -181,7 +181,7 @@ export const VsphereNetworksTable: Component<{
kind="text"
sort={sort}
sortKey="type"
class="md:w-[13%]"
class="w-[35%] md:w-[13%]"
>
Type
</PlatformSortableTableHead>
@@ -196,7 +196,7 @@ export const VsphereNetworksTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="vms"
class="hidden sm:table-cell md:w-[7%]"
class="w-[20%] md:w-[7%]"
>
VMs
</PlatformSortableTableHead>
@@ -280,7 +280,7 @@ export const VsphereNetworksTable: Component<{
<span class="block truncate">{hosts().label}</span>
</TableCell>
<TableCell
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content tabular-nums sm:table-cell`}
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content tabular-nums`}
>
{vmCount(network)}
</TableCell>