diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 700a8fd417..aca905af22 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2318,6 +2318,11 @@ not a replacement status card, CTA band, or page-local nested card. widest label at the narrowest table in each range: a 34rem container for the band, and the table a 768px viewport leaves (about 695px) for md widths. The Docker Images Update check column needs 23 percent and 19 percent for that. + The room comes from columns that lose nothing when narrower: a metric bar + that never shows a sublabel or a collapsed actions menu, never a bar whose + used/total sublabel would disappear. A weighted colgroup whose optional + columns come and go is checked in every visible-column set, as the Docker + container Updates column is in `dockerContainerTableModel.test.ts`. Below a 360-pixel content width, the shared narrow layout promotes identity to 40 percent and may remove one additional lowest-priority context column via `platform-table-narrow-hidden` rather than shrinking names and metric values diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 9804c4c69f..15c3f338e7 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -910,7 +910,14 @@ in full at every width: the Update check column keeps the ~122px its longest badge ("Update available", "Managed by Pulse") needs with cell padding, through a 23 percent tablet-band width taken from Size and a 19 percent md width taken from Host, Used by and Size. An equal band share and the old 16 percent md width cut -the badge off between 600 and 800px. Hidden header/body columns +the badge off between 600 and 800px. The container Updates column likewise keeps +the 74px update states (Update, Current, Pinned, Failed) whole in every layout +mode and optional-column set at the mode's narrowest table; the 100px states +(Check failed, Updating..., Completed) fit in most layouts but can still clip at +a layout's narrow end when several optional columns show. Its room comes only +from the CPU bar, which never shows a sublabel, and the collapsed actions menu, +so every other column, including the memory bar's used/total sublabel, keeps +its width. Hidden header/body columns are symmetric. Swarm rollout tokens render as words with raw state retained in the title; task starts use the shared relative-time primitive with their exact timestamp retained. Native table/model/guardrail tests and the production @@ -1197,7 +1204,11 @@ keeps controller, kind, ready, and issues and demotes Target with Kubernetes name columns hold the chevron, status dot and name inside one track, so their md widths leave the name room for a typical node or service name at a 768px viewport: Nodes 20 percent (with Capacity at 16 so its -cores, memory and pods label fits at 1100px and wider), Events 24, and +cores, memory and pods label fits at 1100px and wider, and Status at 10.5, +taken from CPU at 7.5 so the Degraded, NotReady and Unknown badges fit from +768px and Unschedulable from about 900px while the memory sublabel keeps its +room; the tablet band sizes each Nodes header and Degraded at a 34rem table), +Events 24, and Services, Configuration, Storage, Controllers and Policy 22. Generated names render through `KubernetesNameText`, which splits off the trailing segment (`splitKubernetesNameTail`) so a narrow cell truncates the head and keeps the diff --git a/frontend-modern/browser-verification/6683727b493b9b45.json b/frontend-modern/browser-verification/6683727b493b9b45.json new file mode 100644 index 0000000000..b0ebe078f0 --- /dev/null +++ b/frontend-modern/browser-verification/6683727b493b9b45.json @@ -0,0 +1,116 @@ +{ + "version": 1, + "base_sha": "800329cbd83c48fdb7402a18633189f2c8a2d6ff", + "verified_at": "2026-10-05T14:26:34Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/docker/dockerContainerTableModel.ts", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx" + ], + "content_sha256": { + "frontend-modern/src/features/docker/dockerContainerTableModel.ts": "ff3c0d6fd2339cb7d522cbc6ccc5611b3ffed425877e09388dcc5367e1613320", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "8e1b0ba7668b31acdf62599f538f38714828919a82e430adf5fcd772efa8ae67" + }, + "routes": [ + "/docker/containers", + "/kubernetes/nodes" + ], + "viewports": [ + { + "width": 390, + "height": 900 + }, + { + "width": 430, + "height": 900 + }, + { + "width": 480, + "height": 900 + }, + { + "width": 540, + "height": 900 + }, + { + "width": 573, + "height": 900 + }, + { + "width": 600, + "height": 900 + }, + { + "width": 640, + "height": 900 + }, + { + "width": 700, + "height": 900 + }, + { + "width": 740, + "height": 900 + }, + { + "width": 767, + "height": 900 + }, + { + "width": 768, + "height": 900 + }, + { + "width": 800, + "height": 900 + }, + { + "width": 860, + "height": 900 + }, + { + "width": 900, + "height": 900 + }, + { + "width": 960, + "height": 900 + }, + { + "width": 1024, + "height": 900 + }, + { + "width": 1100, + "height": 900 + }, + { + "width": 1280, + "height": 900 + }, + { + "width": 1440, + "height": 900 + }, + { + "width": 1600, + "height": 900 + } + ], + "states": [ + "Private mock stack (PULSE_MOCK_MODE, random metrics off) serving this tree on main 800329cbd8; before = main, after = main plus this change, measured in the same session", + "Docker containers, grouped by host (default) and ungrouped (Host and Engine columns shown, seeded through localStorage dockerContainersGroupingMode=flat), 18 widths 390 to 1600: the Updates control clipped 5 cells at every width from 573 to 800 grouped (1 at 900) and from 573 to 1024 ungrouped (1 at 1100) on main, none patched", + "Clip sweep over every cell and its overflow-hidden or ellipsis descendants, plus elements spilling past their cell, .sr-only excluded: totals 89 to 53 grouped, 195 to 132 ungrouped, 85 to 78 on Kubernetes Nodes; no column clips more at any width and no header newly clips", + "Metric sublabels: the count of cells showing a used/total sublabel is unchanged per column and width (Docker memory at 800 to 960 and 1440 to 1600, Kubernetes memory from 1024); CPU bars never show one", + "Update state probe (each label cloned into the narrowest Updates cell): Update 74px, Current 69px, Pinned and Failed fit on one line at every width in both views (main cut them from 573 to 900 grouped and to 1100 ungrouped, and wrapped them onto two lines at 480); Check failed (100px) fits from 600 grouped; Review action (112px, shown only while an update awaits review) still clips grouped at 573 to 700 and 800, and ungrouped at most widths below 1280 and at 1600", + "Kubernetes Nodes Status probe: Degraded 64px, NotReady and Unknown fit at every width from 573 (main cut them from 573 to 860 except at 767); Unschedulable 89px fits from 900 and at 1280 and 1440 (main cut it at every width)", + "Model check of every Docker layout mode and optional-column set (State, Restarts, Engine, grouped): every column other than CPU, actions and Updates keeps exactly its main width", + "Before and after element screenshots: Docker containers at 600, 700, 900 and 1440 (Current and Update cut on main, whole patched), Kubernetes Nodes at 600, 768 and 1280 (Degraded cut on main, whole patched)" + ], + "interactions": [ + "Loaded each route fresh, waited for the row count to settle, resized through every viewport before measuring", + "Sorted by Updates (Docker) and Status (Kubernetes) at 600, 768 and 1280: aria-sort ascending, header unclipped, third click restores the built-in order", + "Expanded and collapsed the first row's detail on both tables at 600, 768 and 1280 (aria-expanded true then false, detail visible); no console errors", + "Switched the Docker table to the ungrouped view so the Host and Engine columns render" + ] +} diff --git a/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts b/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts index eb8dd60f5a..27d9ed5829 100644 --- a/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts +++ b/frontend-modern/src/features/docker/__tests__/dockerContainerTableModel.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import type { WorkloadTableLayoutMode } from '@/components/Workloads/guestRowModel'; import { getDockerContainerColumnWidthStyle, getDockerContainerTableMinWidthClass, @@ -139,6 +140,62 @@ describe('dockerContainerTableModel', () => { expect(wideIds).toContain('networks'); expect(wideIds).toContain('mounts'); }); + + it('keeps the update control unclipped in every layout and optional column set', () => { + // Update and Current render about 74px wide, and the cell pads 6px a side + // below a 640px viewport and 8px above. Each layout is checked at its + // narrowest table: the 34rem phone container for mobile (below it the + // badge wraps instead), then 720, 900 and 1440px. Wide rows expand the + // lifecycle controls to about 92px. + const narrowest: [WorkloadTableLayoutMode, number, number][] = [ + ['mobile', 544, 12], + ['tablet', 720, 16], + ['compact', 900, 16], + ['wide', 1440, 16], + ]; + const pixels = ( + columnId: 'updates' | 'actions', + layoutMode: WorkloadTableLayoutMode, + ids: ReturnType, + tableWidth: number, + ) => + (Number.parseFloat( + String( + getDockerContainerColumnWidthStyle( + columnId, + layoutMode, + ids.map((column) => column.id), + ).width, + ), + ) / + 100) * + tableWidth; + for (const [layoutMode, tableWidth, cellPadding] of narrowest) { + for (const includeRuntime of [false, true]) { + for (const includeRestarts of [false, true]) { + for (const includeState of [false, true]) { + for (const groupedByHost of [false, true]) { + const columns = getDockerContainerVisibleColumnsForLayout( + layoutMode, + includeRuntime, + includeRestarts, + includeState, + { groupedByHost }, + ); + expect(pixels('updates', layoutMode, columns, tableWidth)).toBeGreaterThanOrEqual( + 74 + cellPadding, + ); + if (layoutMode === 'wide') { + expect(pixels('actions', layoutMode, columns, tableWidth)).toBeGreaterThanOrEqual( + 92 + cellPadding, + ); + } + } + } + } + } + } + }); }); describe('docker container alert override identity', () => { diff --git a/frontend-modern/src/features/docker/dockerContainerTableModel.ts b/frontend-modern/src/features/docker/dockerContainerTableModel.ts index 74a980d16b..69b1af6bee 100644 --- a/frontend-modern/src/features/docker/dockerContainerTableModel.ts +++ b/frontend-modern/src/features/docker/dockerContainerTableModel.ts @@ -99,20 +99,31 @@ const DOCKER_CONTAINER_COLUMNS: DockerContainerTableColumn[] = [ { id: 'actions', label: 'Actions', kind: 'badge' }, ]; +// The Updates column holds the update control or its status badge, which +// must not clip: Update and Current are about 74px, and Check failed, +// Updating... and Completed up to 100px. Every layout gives the column room +// for the 74px states at its narrowest table width, whichever optional +// columns are showing. The 100px states fit in most layouts too, but can +// still clip at a layout's narrow end when several optional columns show. +// The room comes only from +// the CPU bar, which never shows a sublabel, and from the collapsed actions +// menu, so every other column keeps its width. Below the 34rem phone +// container the badge wraps instead (index.css). Wide rows expand the +// lifecycle controls to about 92px, so actions gains a little there. const DOCKER_CONTAINER_DESKTOP_WIDTHS: Record = { container: 16, host: 8, runtime: 7, image: 16, state: 6, - cpu: 9, + cpu: 6, memory: 10, restarts: 6, ports: 10, networks: 8, mounts: 9, - updates: 7, - actions: 8, + updates: 9, + actions: 9, }; const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< @@ -136,20 +147,20 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< mobile: { container: 30, state: 12, - cpu: 14, + cpu: 10, memory: 16, restarts: 8, - updates: 10, - actions: 10, + updates: 16.25, + actions: 7.75, }, tablet: { container: 27, host: 15, state: 10, - cpu: 15, + cpu: 9.5, memory: 19, restarts: 9, - updates: 9, + updates: 14.5, actions: 6, }, // Compact is what a 1280-1536px laptop window gets. Host, Engine and the @@ -161,12 +172,12 @@ const DOCKER_CONTAINER_RESPONSIVE_WIDTHS: Record< runtime: 9.5, image: 16.5, state: 7, - cpu: 9.5, + cpu: 8, memory: 10, restarts: 8.5, ports: 12, - updates: 10, - actions: 8, + updates: 13.5, + actions: 6, }, }; diff --git a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx index f752960f08..0d939ae8c9 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx @@ -208,17 +208,26 @@ export const KubernetesNodesTable: Component<{ Desktop widths: Node gets headroom for cluster-style names, Runtime gets room for "containerd://1.7.20" -style values, Capacity gets room for "6 cores / - 51.0 GB / 110 pods" strings, CPU and Memory bars share - an equal slice, and the short-text columns (Cluster, - Roles, Kubelet, Uptime) trim accordingly. Wide + 51.0 GB / 110 pods" strings, and the short-text columns + (Cluster, Roles, Kubelet, Uptime) trim accordingly. Wide desktop gets extra room without forcing normal desktop viewports to hide Capacity behind horizontal scroll. + The Status badge must not clip: Degraded and NotReady + need about 64px and Unschedulable about 89px. Status + takes its md room from CPU, whose bar never shows a + sublabel, so every other column keeps its width and + Memory keeps room for its used/total sublabel. + Unschedulable fits from a ~800px table and from 1280px, + where Kubelet, Runtime and Uptime appear. bandWidth sizes + the tablet band for each header and for Degraded at a + 34rem table. */} Node @@ -232,7 +241,8 @@ export const KubernetesNodesTable: Component<{ kind="text" sort={sort} sortKey="status" - class="platform-table-phone-hidden md:w-[8%]" + class="platform-table-phone-hidden md:w-[10.5%]" + bandWidth={14.6} > Status @@ -241,6 +251,7 @@ export const KubernetesNodesTable: Component<{ sort={sort} sortKey="cluster" class="platform-table-mobile-w-15 md:w-[8%]" + bandWidth={12.1} > @@ -249,6 +260,7 @@ export const KubernetesNodesTable: Component<{ sort={sort} sortKey="roles" class="platform-table-phone-hidden md:w-[8%]" + bandWidth={9.2} > @@ -272,7 +284,8 @@ export const KubernetesNodesTable: Component<{ kind="metric-bar" sort={sort} sortKey="cpu" - class="platform-table-mobile-w-15 md:w-[10%]" + class="platform-table-mobile-w-15 md:w-[7.5%]" + bandWidth={9.2} > CPU @@ -281,6 +294,7 @@ export const KubernetesNodesTable: Component<{ sort={sort} sortKey="memory" class="platform-table-mobile-w-15 md:w-[10%]" + bandWidth={11.8} > @@ -297,6 +311,7 @@ export const KubernetesNodesTable: Component<{ sort={sort} sortKey="capacity" class="platform-table-mobile-w-10 md:w-[16%]" + bandWidth={13.1} > 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 61a4dd9678..6a1f1e2152 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -369,11 +369,20 @@ describe('platform overview layout guardrails', () => { /sortKey="roles"[\s\S]{0,160}?class="platform-table-phone-hidden md:w-\[8%\]"/, ); // Status names the node state on desktop; the dot carries it on phones. + // Its badge must not clip (Degraded ~64px, Unschedulable ~89px), so Status + // takes md room from CPU, whose bar has no sublabel, and the tablet band + // sizes every header and Degraded at a 34rem table. expect(kubernetesNodesTableSource).toMatch( - /sortKey="status"[\s\S]{0,120}?class="platform-table-phone-hidden md:w-\[8%\]"/, + /sortKey="status"[\s\S]{0,120}?class="platform-table-phone-hidden md:w-\[10\.5%\]"\s*bandWidth=\{14\.6\}/, ); expect(kubernetesNodesTableSource).toMatch( - /sortKey="capacity"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[16%\]"/, + /sortKey="cpu"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[7\.5%\]"\s*bandWidth=\{9\.2\}/, + ); + expect(kubernetesNodesTableSource).toMatch( + /sortKey="memory"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[10%\]"\s*bandWidth=\{11\.8\}/, + ); + expect(kubernetesNodesTableSource).toMatch( + /sortKey="capacity"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[16%\]"\s*bandWidth=\{13\.1\}/, ); expect(kubernetesDeploymentsTableSource).toMatch( /sortKey="namespace"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[18%\]"/,