From ff21393c8007bf11dc50ca35d98a720866a646a0 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:11:17 +0100 Subject: [PATCH] Truncate platform summary cells inside their own column Follow-up to #2509, which fixed the Kubernetes tables. Docker, Proxmox and vSphere summary cells still wrapped long values in a capped inline-block span (max-w-48 truncate, max-w-[18rem] truncate, or truncate inline-block). The cap ignored the column width of these fixed-layout tables, so a long value ran past a narrower cell with no ellipsis (a swarm service image by 225px at 700 wide, volume mountpoints by 51px at 1440), and the inline-block sat about 2px above the row's text line. A block truncate span fills the cell, ends in an ellipsis inside it and sits on the row line. The Docker service Update cell keeps its phone wrap and truncates inside its column from md instead of at a 10rem cap. The guardrail from #2509 now covers every platform table source that carried the pattern, and the coverage table test pins the Node cell. --- .../subsystems/frontend-primitives.md | 7 ++ .../internal/subsystems/storage-recovery.md | 11 +++ .../internal/subsystems/unified-resources.md | 7 +- .../737e5f5d4595888b.json | 90 +++++++++++++++++++ .../features/docker/DockerConfigsTable.tsx | 4 +- .../src/features/docker/DockerImagesTable.tsx | 5 +- .../features/docker/DockerNetworksTable.tsx | 2 +- .../features/docker/DockerSecretsTable.tsx | 4 +- .../features/docker/DockerServicesTable.tsx | 6 +- .../src/features/docker/DockerTasksTable.tsx | 2 +- .../features/docker/DockerVolumesTable.tsx | 4 +- .../platformOverviewLayout.guardrails.test.ts | 13 ++- .../features/proxmox/ProxmoxCoverageTable.tsx | 9 +- .../proxmox/ProxmoxMailGatewayDrawer.tsx | 2 +- .../proxmox/ProxmoxRecoverableTable.tsx | 7 +- .../__tests__/ProxmoxCoverageTable.test.tsx | 28 ++++++ .../src/features/vmware/VsphereHostsTable.tsx | 2 +- 17 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 frontend-modern/browser-verification/737e5f5d4595888b.json diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c866d04df2..03a9c7ad7d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2340,6 +2340,13 @@ not a replacement status card, CTA band, or page-local nested card. disclosure; a non-expandable identity cell must otherwise expose its complete value without requiring hover. Default data rows stay single-line so scan density and row rhythm are not traded for automatic text wrapping. + A summary value that must truncate sits in a `block truncate` span as the + cell's only child, so it fills the fixed-layout column and ends in an + ellipsis inside it on the row's text line. A capped `inline-block max-w-* + truncate` span ignores the column width: it spills past a narrower cell + with no ellipsis, wastes room in a wider one, and sits about 2 pixels above + the row baseline. `platformOverviewLayout.guardrails.test.ts` holds the + Docker, Kubernetes, Proxmox and vSphere table sources to that rule. When a platform table row itself owns that keyboard- and touch-operable disclosure, `SummaryRowActionButton` removes the redundant visible chevron below the shared mobile breakpoint while preserving the button for screen diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index ed3f3e4803..dd5250c085 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -3094,6 +3094,17 @@ presentation and sort change only; it adds no backup, restore, or retention authority. Pinned by `proxmoxBackupRecoveryModel.test.ts` and `ProxmoxCoverageTable.test.tsx`. +### Backup table values truncate inside their own column + +The coverage Node cell, the restore-evidence Location and Details cells, and +the recoverable table's Location and Details cells render their value in a +block span that fills the fixed-layout cell, so a long node name, datastore path +or detail ends in an ellipsis inside its column on the row's text line, with the +full value on the title. A capped inline-block span let a long detail run past +its cell with no ellipsis and sat above the row. This is presentation only; it +adds no backup, restore, or retention authority. Pinned by +`ProxmoxCoverageTable.test.tsx` and `platformOverviewLayout.guardrails.test.ts`. + ### Retained Patrol objectives do not create recovery authority The shared `internal/api` retained-objective endpoints may scope an outcome to diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index b21e212e96..3ebdb085bb 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1189,7 +1189,12 @@ RBAC names first, then the namespace scope, then the short Lifecycle and Data shape values, then Kind. Kubernetes summary cells truncate through a block span that fills the cell, so a long value ends in an ellipsis inside its own column on the row's text line; a fixed-width inline-block cap spilled past narrow -cells without an ellipsis and sat above the row baseline. +cells without an ellipsis and sat above the row baseline. Docker image, network, +volume, service, task, secret and config cells, Proxmox backup coverage, +restore-evidence and recoverable cells, the Mail Gateway drawer's top domains, +and vSphere host vCenter cells follow the same rule. The Docker service Update +cell still wraps on phones and from md truncates inside its column instead of +at a 10rem cap. Duration and interval cells keep the same split: unified-resource or source-specific consumers own which elapsed duration, human fallback, or poll interval field is meaningful, while dense platform table rendering must use diff --git a/frontend-modern/browser-verification/737e5f5d4595888b.json b/frontend-modern/browser-verification/737e5f5d4595888b.json new file mode 100644 index 0000000000..172f9537e5 --- /dev/null +++ b/frontend-modern/browser-verification/737e5f5d4595888b.json @@ -0,0 +1,90 @@ +{ + "version": 1, + "base_sha": "914fabfc1fef6d728bf145cf3097557ef82eafe7", + "verified_at": "2026-10-05T13:10:50Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/docker/DockerConfigsTable.tsx", + "frontend-modern/src/features/docker/DockerImagesTable.tsx", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx", + "frontend-modern/src/features/docker/DockerServicesTable.tsx", + "frontend-modern/src/features/docker/DockerTasksTable.tsx", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx" + ], + "content_sha256": { + "frontend-modern/src/features/docker/DockerConfigsTable.tsx": "5e5aa4ec47bff613fa1e61fffc6e5f78fbb52a11e74fd692f632f7d9f39c68ed", + "frontend-modern/src/features/docker/DockerImagesTable.tsx": "dad1a0a668160d0590d376d35da94f0e2e500965852631fff40e85a09ece62c1", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "a4fbc6cfe58dabb7e453e0e65b6bc6aeb72bcc05945fb68a6571e503f44722c9", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx": "3a68466cc647a84f4bc7cf9d9b6184ebf0476b9f21d05b9329ae400cfd9f7460", + "frontend-modern/src/features/docker/DockerServicesTable.tsx": "aa3cf92e9aeb611027cec10dc3596e81907732e20930c06ba5d9275451f42c1e", + "frontend-modern/src/features/docker/DockerTasksTable.tsx": "02435077ae71767e6f6db6bc0f3441a585661013dd62c35892e48229cc53e7b6", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx": "020345e4bc6c9a4389410cbfbe566aa34dae8ea7c702f65246bdfb75ce9e3e96", + "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "b16eaab7cc00f13a858b290f453e46ee1886c1e50a9a542999f4568a0d42aead", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "18c6dbea9038cc6561f5f914f61e67954abc4a3a93f83560b76398ae2915b7ba", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx": "f8f3695bbd200fa8a8f7091ec6431faa1dada5fbdbb98b80b9fc8c5f7ede304b", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "e6caa80836b45e9509d450e6816645062adc1cf6ce29041810e5daf70491ac6f" + }, + "routes": [ + "/docker/images", + "/docker/networks", + "/docker/storage", + "/docker/swarm", + "/proxmox/backups/date", + "/proxmox/backups/coverage", + "/proxmox/mail", + "/vmware/overview" + ], + "viewports": [ + { + "width": 390, + "height": 1000 + }, + { + "width": 600, + "height": 1000 + }, + { + "width": 700, + "height": 1000 + }, + { + "width": 768, + "height": 1000 + }, + { + "width": 900, + "height": 1000 + }, + { + "width": 1100, + "height": 1000 + }, + { + "width": 1280, + "height": 1000 + }, + { + "width": 1440, + "height": 1000 + } + ], + "states": [ + "Live DOM probe on main at 700 and 1440 over every changed span (Docker images, networks, volumes, services, tasks, secrets, configs; Proxmox recoverable, coverage Node and restore-evidence Location/Details, Mail Gateway drawer Top domains; vSphere hosts vCenter): all 1000 measured spans are the only child of their cell and every table, including the drawer's Top domains table, computes table-layout fixed", + "Main: 66 spans ran past their cell with no ellipsis (Service Image 225px and Secret Labels 184px at 700, Mountpoint 51px and restore-evidence Details 22px at 1440, PMG domains 71px at 700), and every inline-block span sat 2 to 2.5px above the row's text line", + "Patched: no span runs past its cell, all 134 truncated values end in an ellipsis inside their own column, and each value sits on the row's text line (0px; the 11px restore-evidence rows read 0.5px)", + "Docker service Update cell still wraps Rollback started onto two lines below md (700) and truncates inside its column from md (1440) instead of at a 10rem cap", + "Clip sweep (scrollWidth over clientWidth on every cell, screen-reader text excluded) across all eight routes at 390 to 1440: clipped cells 931 on main and 928 patched over 19209 measured cells, no column worse at any width (Secret Labels, Mountpoint and Used by each one better)", + "Before and after element screenshots of all eleven affected tables at 700 and 1440 show hard cuts such as registry.exa and mail-relay.customer-opera on main becoming registry... and mail-relay.customer-op... inside the cell, with Created and Mountpoint back on the row line", + "Mock PMG reports no domain stats, so the drawer's Top domains table was mounted by adding eight domains (4 to 54 characters) to the real /api/pmg/instances response through a Playwright route intercept" + ], + "interactions": [ + "Expanded the first three Proxmox coverage rows to mount the restore-evidence table", + "Opened the first Mail Gateway row's drawer to mount the Top domains table", + "Loaded each Docker, Proxmox backups (by date and coverage) and vSphere overview tab fresh at every viewport and waited for the row count to settle before measuring" + ] +} diff --git a/frontend-modern/src/features/docker/DockerConfigsTable.tsx b/frontend-modern/src/features/docker/DockerConfigsTable.tsx index 3ff681edc6..a3314ced4f 100644 --- a/frontend-modern/src/features/docker/DockerConfigsTable.tsx +++ b/frontend-modern/src/features/docker/DockerConfigsTable.tsx @@ -194,7 +194,7 @@ export const DockerConfigsTable: Component = (props) => class={`${getPlatformTableCellClassForKind('text')} text-base-content`} > = (props) => class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`} > {dockerLabelsSummary(resource.docker?.labels)} diff --git a/frontend-modern/src/features/docker/DockerImagesTable.tsx b/frontend-modern/src/features/docker/DockerImagesTable.tsx index 744bc18b71..454a028d53 100644 --- a/frontend-modern/src/features/docker/DockerImagesTable.tsx +++ b/frontend-modern/src/features/docker/DockerImagesTable.tsx @@ -221,10 +221,7 @@ export const DockerImagesTable: Component< - + {operational().consumerSummary} diff --git a/frontend-modern/src/features/docker/DockerNetworksTable.tsx b/frontend-modern/src/features/docker/DockerNetworksTable.tsx index db79aef671..8d6ff264a2 100644 --- a/frontend-modern/src/features/docker/DockerNetworksTable.tsx +++ b/frontend-modern/src/features/docker/DockerNetworksTable.tsx @@ -569,7 +569,7 @@ export const DockerNetworksTable: Component = (props) - + {subnets()} diff --git a/frontend-modern/src/features/docker/DockerSecretsTable.tsx b/frontend-modern/src/features/docker/DockerSecretsTable.tsx index 4e6c69465d..95a433a05e 100644 --- a/frontend-modern/src/features/docker/DockerSecretsTable.tsx +++ b/frontend-modern/src/features/docker/DockerSecretsTable.tsx @@ -209,7 +209,7 @@ export const DockerSecretsTable: Component = (props) => class={`${getPlatformTableCellClassForKind('text')} text-base-content`} > = (props) => class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`} > {dockerLabelsSummary(resource.docker?.labels)} diff --git a/frontend-modern/src/features/docker/DockerServicesTable.tsx b/frontend-modern/src/features/docker/DockerServicesTable.tsx index efb9d51923..596f0d8d22 100644 --- a/frontend-modern/src/features/docker/DockerServicesTable.tsx +++ b/frontend-modern/src/features/docker/DockerServicesTable.tsx @@ -331,14 +331,14 @@ export const DockerServicesTable: Component<{ - + {image()} @@ -361,7 +361,7 @@ export const DockerServicesTable: Component<{ class={`${getPlatformTableCellClassForKind('text')} text-base-content`} > {update().label} diff --git a/frontend-modern/src/features/docker/DockerTasksTable.tsx b/frontend-modern/src/features/docker/DockerTasksTable.tsx index e45f4a182b..10be8b87a6 100644 --- a/frontend-modern/src/features/docker/DockerTasksTable.tsx +++ b/frontend-modern/src/features/docker/DockerTasksTable.tsx @@ -238,7 +238,7 @@ export const DockerTasksTable: Component = (props) => { class={`${getPlatformTableCellClassForKind('text')} text-base-content`} > = (props) => class={`${getPlatformTableCellClassForKind('text')} text-base-content`} > = (props) => class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`} > {dockerTextValue(resource.docker?.mountpoint)} diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index 22a4c5545c..f55f91896a 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -395,11 +395,22 @@ describe('platform overview layout guardrails', () => { ); }); - it('keeps Kubernetes summary cells truncating inside their own column', () => { + it('keeps platform summary cells truncating inside their own column', () => { // A capped inline-block (max-w-52 truncate) ignored the column width, so a // long value spilled past a narrow cell without an ellipsis and sat above // the row's text line; a block span fills the cell and ellipsizes there. for (const source of [ + dockerConfigsTableSource, + dockerImagesTableSource, + dockerNetworksTableSource, + dockerSecretsTableSource, + dockerServicesTableSource, + dockerTasksTableSource, + dockerVolumesTableSource, + proxmoxCoverageTableSource, + proxmoxMailGatewayDrawerSource, + proxmoxRecoverableTableSource, + vsphereHostsTableSource, kubernetesAutoscalingTableSource, kubernetesConfigTableSource, kubernetesControllersTableSource, diff --git a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx index 34233d7e81..8bce8e7698 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx @@ -350,7 +350,7 @@ export function ProxmoxCoverageTable(props: { fallback={—} > {(node) => ( - + {node()} )} @@ -586,10 +586,7 @@ export function ProxmoxCoverageTable(props: { - + {artifact.location} @@ -634,7 +631,7 @@ export function ProxmoxCoverageTable(props: { class={`${getPlatformTableCellClassForKind('text')} text-base-content`} > {artifact.detail || '—'} diff --git a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx index 94390fc24f..bca5931740 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx @@ -649,7 +649,7 @@ export const ProxmoxMailGatewayDrawer: Component<{ - + {domain.domain || '—'} diff --git a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx index f5a514a569..ff15869e3f 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx @@ -160,7 +160,7 @@ export function ProxmoxRecoverableTable(props: { - + {artifact.location} @@ -192,10 +192,7 @@ export function ProxmoxRecoverableTable(props: { - + {artifact.detail || '—'} diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx index a051faf0f4..87b2605ca3 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx @@ -258,4 +258,32 @@ describe('ProxmoxCoverageTable column visibility', () => { ); expect(titles.join(' ')).not.toMatch(/canonical|provider evidence|independent recovery/); }); + + it('keeps a long node name truncating inside its own column', () => { + // A capped inline-block sat above the row's text line; a block span fills + // the fixed-layout cell and ends in an ellipsis there. + const node = 'pve-production-rack-04-node-17.example.internal'; + render(() => ( + } + emptyTitle="" + emptyDescription="" + sortKey={(() => 'posture') as Accessor} + sortDirection={() => 'asc'} + onSort={() => {}} + expandedKeys={new Set()} + onToggleExpand={() => {}} + showTaskColumn={false} + layoutWidth={() => 1_200} + /> + )); + + const span = document.querySelector(`td span[title="${node}"]`); + expect(span).toHaveTextContent(node); + expect(span?.parentElement?.children).toHaveLength(1); + expect(span?.getAttribute('class')).toBe('block truncate'); + expect(span?.closest('table')).toHaveClass('table-fixed'); + }); }); diff --git a/frontend-modern/src/features/vmware/VsphereHostsTable.tsx b/frontend-modern/src/features/vmware/VsphereHostsTable.tsx index aadbe5d836..965903f9a3 100644 --- a/frontend-modern/src/features/vmware/VsphereHostsTable.tsx +++ b/frontend-modern/src/features/vmware/VsphereHostsTable.tsx @@ -503,7 +503,7 @@ export const VsphereHostsTable: Component<{