Incorporate batch-start upstream badge repairs

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-05 16:00:31 +01:00
7 changed files with 244 additions and 20 deletions
@@ -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
@@ -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
@@ -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"
]
}
@@ -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<typeof getDockerContainerVisibleColumnsForLayout>,
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', () => {
@@ -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<DockerContainerTableColumnId, number> = {
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,
},
};
@@ -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.
*/}
<PlatformSortableTableHead
kind="name"
sort={sort}
sortKey="node"
class="platform-table-mobile-w-30 md:w-[20%]"
bandWidth={30}
>
Node
</PlatformSortableTableHead>
@@ -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
</PlatformSortableTableHead>
@@ -241,6 +251,7 @@ export const KubernetesNodesTable: Component<{
sort={sort}
sortKey="cluster"
class="platform-table-mobile-w-15 md:w-[8%]"
bandWidth={12.1}
>
<PlatformResponsiveTableLabel compact="Clus" full="Cluster" />
</PlatformSortableTableHead>
@@ -249,6 +260,7 @@ export const KubernetesNodesTable: Component<{
sort={sort}
sortKey="roles"
class="platform-table-phone-hidden md:w-[8%]"
bandWidth={9.2}
>
<PlatformResponsiveTableLabel compact="Role" full="Roles" />
</PlatformSortableTableHead>
@@ -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
</PlatformSortableTableHead>
@@ -281,6 +294,7 @@ export const KubernetesNodesTable: Component<{
sort={sort}
sortKey="memory"
class="platform-table-mobile-w-15 md:w-[10%]"
bandWidth={11.8}
>
<PlatformResponsiveTableLabel compact="Mem" full="Memory" />
</PlatformSortableTableHead>
@@ -297,6 +311,7 @@ export const KubernetesNodesTable: Component<{
sort={sort}
sortKey="capacity"
class="platform-table-mobile-w-10 md:w-[16%]"
bandWidth={13.1}
>
<PlatformResponsiveTableLabel compact="Cap" full="Capacity" />
</PlatformSortableTableHead>
@@ -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%\]"/,