mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -194,7 +194,7 @@ export const DockerConfigsTable: Component<DockerNativeTableProps> = (props) =>
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-56 truncate"
|
||||
class="block truncate"
|
||||
title={dockerTextValue(resource.docker?.objectCreatedAt)}
|
||||
>
|
||||
<PlatformTableRelativeTimeValue
|
||||
@@ -206,7 +206,7 @@ export const DockerConfigsTable: Component<DockerNativeTableProps> = (props) =>
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-88 truncate"
|
||||
class="block truncate"
|
||||
title={dockerLabelsSummary(resource.docker?.labels)}
|
||||
>
|
||||
{dockerLabelsSummary(resource.docker?.labels)}
|
||||
|
||||
@@ -221,10 +221,7 @@ export const DockerImagesTable: Component<
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content platform-table-phone-hidden`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[20rem] truncate"
|
||||
title={operational().consumerSummary}
|
||||
>
|
||||
<span class="block truncate" title={operational().consumerSummary}>
|
||||
{operational().consumerSummary}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -569,7 +569,7 @@ export const DockerNetworksTable: Component<DockerNetworksTableProps> = (props)
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-[18rem] truncate" title={subnets()}>
|
||||
<span class="block truncate" title={subnets()}>
|
||||
{subnets()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -209,7 +209,7 @@ export const DockerSecretsTable: Component<DockerNativeTableProps> = (props) =>
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-48 truncate"
|
||||
class="block truncate"
|
||||
title={dockerTextValue(resource.docker?.objectCreatedAt)}
|
||||
>
|
||||
<PlatformTableRelativeTimeValue
|
||||
@@ -221,7 +221,7 @@ export const DockerSecretsTable: Component<DockerNativeTableProps> = (props) =>
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-phone-hidden text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[16rem] truncate"
|
||||
class="block truncate"
|
||||
title={dockerLabelsSummary(resource.docker?.labels)}
|
||||
>
|
||||
{dockerLabelsSummary(resource.docker?.labels)}
|
||||
|
||||
@@ -331,14 +331,14 @@ export const DockerServicesTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} platform-table-narrow-hidden hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="truncate inline-block max-w-32" title={stack()}>
|
||||
<span class="block truncate" title={stack()}>
|
||||
{stack()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="truncate inline-block max-w-[18rem]" title={image()}>
|
||||
<span class="block truncate" title={image()}>
|
||||
{image()}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -361,7 +361,7 @@ export const DockerServicesTable: Component<{
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-full whitespace-normal wrap-break-word leading-3 md:max-w-40 md:truncate md:leading-normal"
|
||||
class="block whitespace-normal wrap-break-word leading-3 md:truncate md:leading-normal"
|
||||
title={update().title}
|
||||
>
|
||||
{update().label}
|
||||
|
||||
@@ -238,7 +238,7 @@ export const DockerTasksTable: Component<DockerNativeTableProps> = (props) => {
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-56 truncate"
|
||||
class="block truncate"
|
||||
title={dockerTextValue(
|
||||
resource.docker?.error ||
|
||||
resource.docker?.message ||
|
||||
|
||||
@@ -246,7 +246,7 @@ export const DockerVolumesTable: Component<DockerNativeTableProps> = (props) =>
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-48 truncate"
|
||||
class="block truncate"
|
||||
title={formatPlatformTableDateTimeValue(resource.docker?.createdAt)}
|
||||
>
|
||||
<PlatformTableRelativeTimeValue
|
||||
@@ -259,7 +259,7 @@ export const DockerVolumesTable: Component<DockerNativeTableProps> = (props) =>
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-88 truncate"
|
||||
class="block truncate"
|
||||
title={dockerTextValue(resource.docker?.mountpoint)}
|
||||
>
|
||||
{dockerTextValue(resource.docker?.mountpoint)}
|
||||
|
||||
+12
-1
@@ -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,
|
||||
|
||||
@@ -350,7 +350,7 @@ export function ProxmoxCoverageTable(props: {
|
||||
fallback={<span class="text-muted">—</span>}
|
||||
>
|
||||
{(node) => (
|
||||
<span class="inline-block max-w-full truncate" title={node()}>
|
||||
<span class="block truncate" title={node()}>
|
||||
{node()}
|
||||
</span>
|
||||
)}
|
||||
@@ -586,10 +586,7 @@ export function ProxmoxCoverageTable(props: {
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[18rem] truncate"
|
||||
title={artifact.location}
|
||||
>
|
||||
<span class="block truncate" title={artifact.location}>
|
||||
{artifact.location}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -634,7 +631,7 @@ export function ProxmoxCoverageTable(props: {
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
>
|
||||
<span
|
||||
class="inline-block max-w-[24rem] truncate"
|
||||
class="block truncate"
|
||||
title={artifact.detailTitle ?? artifact.detail}
|
||||
>
|
||||
{artifact.detail || '—'}
|
||||
|
||||
@@ -649,7 +649,7 @@ export const ProxmoxMailGatewayDrawer: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('name')} font-mono text-[11px] text-base-content`}
|
||||
>
|
||||
<span class="inline-block max-w-56 truncate" title={domain.domain}>
|
||||
<span class="block truncate" title={domain.domain}>
|
||||
{domain.domain || '—'}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -160,7 +160,7 @@ export function ProxmoxRecoverableTable(props: {
|
||||
</TableCell>
|
||||
<Show when={columnVisible('location')}>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('text')} text-base-content`}>
|
||||
<span class="inline-block max-w-[16rem] truncate" title={artifact.location}>
|
||||
<span class="block truncate" title={artifact.location}>
|
||||
{artifact.location}
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -192,10 +192,7 @@ export function ProxmoxRecoverableTable(props: {
|
||||
</TableCell>
|
||||
<Show when={columnVisible('details')}>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('text')} text-base-content`}>
|
||||
<span
|
||||
class="inline-block max-w-[20rem] truncate"
|
||||
title={artifact.detailTitle ?? artifact.detail}
|
||||
>
|
||||
<span class="block truncate" title={artifact.detailTitle ?? artifact.detail}>
|
||||
{artifact.detail || '—'}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -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(() => (
|
||||
<ProxmoxCoverageTable
|
||||
rows={[{ ...row, workload: { ...row.workload, node } }] as WorkloadCoverageRow[]}
|
||||
hasAnyRows
|
||||
emptyIcon={<span />}
|
||||
emptyTitle=""
|
||||
emptyDescription=""
|
||||
sortKey={(() => 'posture') as Accessor<CoverageSortKey>}
|
||||
sortDirection={() => 'asc'}
|
||||
onSort={() => {}}
|
||||
expandedKeys={new Set<string>()}
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -503,7 +503,7 @@ export const VsphereHostsTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden font-mono text-[11px] text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-48 truncate" title={vcenter()}>
|
||||
<span class="block truncate" title={vcenter()}>
|
||||
{vcenter()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user