From b0341768c89d4fe4d612729486e7c34229109b51 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:01:15 +0100 Subject: [PATCH] Stop Kubernetes Config and Deployments tables clipping names Between the phone projection and the md column widths (a 34rem container below a 768px viewport) the Configuration and Deployments tables split their width evenly, so names were cut to a few letters beside short counts. Headers can now declare a percent width for that band through bandWidth / platformTableBandWidthStyle, and both tables size names first. With one cluster in view every scope cell repeated the cluster name and crowded out the namespace that tells rows apart, so scope cells show the namespace alone and keep the full cluster/namespace scope on the title and for assistive technology. Several clusters keep the combined label. Deployments Desired returns only with wide tables, since Ready already reads ready over desired, and Ready keeps its denominator until Desired is visible. On a phone Age takes Ready's spare width so "18h ago" fits. Summary cells used a capped inline-block span that ignored the column width: long values spilled past narrow cells with no ellipsis and sat above the row's text line. A block span fills the cell, ellipsizes there and aligns with its neighbours. --- .../subsystems/frontend-primitives.md | 5 + .../internal/subsystems/unified-resources.md | 13 ++ .../3fa0808818f027a1.json | 122 ++++++++++++++++++ .../src/__tests__/App.architecture.test.ts | 19 +++ .../kubernetes/KubernetesAutoscalingTable.tsx | 18 ++- .../kubernetes/KubernetesConfigTable.tsx | 31 +++-- .../kubernetes/KubernetesControllersTable.tsx | 16 ++- .../kubernetes/KubernetesDeploymentsTable.tsx | 22 +++- .../kubernetes/KubernetesEventsTable.tsx | 24 ++-- .../kubernetes/KubernetesNetworkingTable.tsx | 21 +-- .../kubernetes/KubernetesNodesTable.tsx | 2 +- .../kubernetes/KubernetesPodsTable.tsx | 12 +- .../kubernetes/KubernetesPolicyTable.tsx | 35 +++-- .../kubernetes/KubernetesScopeText.tsx | 28 ++++ .../kubernetes/KubernetesServicesTable.tsx | 21 +-- .../kubernetes/KubernetesStorageTable.tsx | 19 +-- .../KubernetesAutoscalingTable.test.tsx | 5 +- .../KubernetesDeploymentsTable.test.tsx | 35 +++++ .../__tests__/KubernetesPodsTable.test.tsx | 32 ++++- .../__tests__/kubernetesPageModel.test.ts | 48 +++++++ .../kubernetes/kubernetesPageModel.ts | 19 +++ .../platformOverviewLayout.guardrails.test.ts | 38 +++++- .../__tests__/sharedPlatformPage.test.ts | 7 + .../platformPage/sharedPlatformPage.tsx | 12 ++ frontend-modern/src/index.css | 20 ++- 25 files changed, 525 insertions(+), 99 deletions(-) create mode 100644 frontend-modern/browser-verification/3fa0808818f027a1.json create mode 100644 frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index bd47fb5512..c866d04df2 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2309,6 +2309,11 @@ not a replacement status card, CTA band, or page-local nested card. keeps the 30 percent `platform-table-name-column` anchor in that band. Tables sized by a weighted colgroup keep their own widths there, since column widths win over header widths. + A table whose columns need unequal room in that band (a long name beside + short counts) declares each header's percent through + `platformTableBandWidthStyle` or the sortable header's `bandWidth`, which + sets `--platform-table-band-width`; the shell applies it only inside the band, + after the name anchor, so md widths and phone widths are unchanged. 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 26a302151d..b21e212e96 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1177,6 +1177,19 @@ part that tells siblings apart (`checkout-api-6d…-x7k2p`, `prod…-03`), with full name on the title. Pod names, the Pods table's Node column and controller names use it. Deployment names are chosen by people, so they keep plain end truncation. +With one cluster in view, every Kubernetes scope cell repeated the cluster +name and crowded out the namespace that differs between rows, so the tables +show the namespace alone (`Cluster` for cluster-scoped objects) through +`kubernetesScopeDisplayLabel` and keep the full cluster/namespace scope on the +title; several clusters keep the combined label. The Deployments table keeps +Desired for wide tables only, because Ready already shows ready over desired, +and sizes the tablet band for a long name beside short counts. The +Configuration tables size their band and md widths for ConfigMap, Secret and +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. 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/3fa0808818f027a1.json b/frontend-modern/browser-verification/3fa0808818f027a1.json new file mode 100644 index 0000000000..7b7931d598 --- /dev/null +++ b/frontend-modern/browser-verification/3fa0808818f027a1.json @@ -0,0 +1,122 @@ +{ + "version": 1, + "base_sha": "3af37a88625701f6dad057fc8889d09a61dffd24", + "verified_at": "2026-10-05T11:01:05Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx", + "frontend-modern/src/features/kubernetes/kubernetesPageModel.ts", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/index.css" + ], + "content_sha256": { + "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "b66c8c5c705b9478d98ba2bfd51ff199c07619ecf40516d7f276a13c5409a825", + "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "bc3e01de166d4467e7b547a72958091c465768ef552ae31118969d6b4d6c36fd", + "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "b1b184557d03331d8d2ef19d9ab8af9d083013ac3b839fde719bd5d5e4d12d9e", + "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "95cca80263abaf11c0025f1c0bf7a2ccc4421acb9a0d2c611096602353ea75d5", + "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "814ce1c8358cdde955da9707498d491bc3081dded52e86405edd9517e3f71f65", + "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "6a0c1e57cddf59acaf0f0a21690fde5db75f08a9fddd4ac957086b5d7ac307c9", + "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "487541b013886c3f371762a585443ae790d6ec7c72531ac7991ec1a5c76b73a5", + "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "5ed2a52f1a7e5c27a96d2df832d4b80ef210873c84051eb396d4f9cf31f42f51", + "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "91293a32d6cb195a07d39f472c9a7e204bd96de709c65d82728e30d74aac77a9", + "frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx": "50a0468caf73eb7658bbc90e26cd5b48702af7a441e384ab70fde5a6a0daf557", + "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "48ccb976014015513b1aa52391da969604c541d2f27a20d8efac098d24890f15", + "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "b0c87e0386b0d4f7440a7859a106d73daf611867bfd88418e1c6ed7cf8456ae9", + "frontend-modern/src/features/kubernetes/kubernetesPageModel.ts": "9cf09fca38e4c64616e1cd4ee578c6c03bddb810498abdd974e0450a68aa81b1", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "843a1efe473f4092a1ee508bd1634bc51fdcce395e2fe7dd41998ba1c7aebf8b", + "frontend-modern/src/index.css": "ce8f3f0d5a8da290fad1545eedca6a46d4211b4e98239fb1a8e4773cf063e4b2" + }, + "routes": [ + "/kubernetes/overview", + "/kubernetes/nodes", + "/kubernetes/workloads", + "/kubernetes/services", + "/kubernetes/storage", + "/kubernetes/configuration", + "/kubernetes/events" + ], + "viewports": [ + { + "width": 360, + "height": 900 + }, + { + "width": 390, + "height": 1100 + }, + { + "width": 430, + "height": 900 + }, + { + "width": 520, + "height": 900 + }, + { + "width": 600, + "height": 1000 + }, + { + "width": 650, + "height": 1000 + }, + { + "width": 700, + "height": 1000 + }, + { + "width": 767, + "height": 1000 + }, + { + "width": 768, + "height": 1000 + }, + { + "width": 820, + "height": 1000 + }, + { + "width": 900, + "height": 1000 + }, + { + "width": 1100, + "height": 1000 + }, + { + "width": 1300, + "height": 1000 + }, + { + "width": 1440, + "height": 1000 + } + ], + "states": [ + "Clip sweep (scrollWidth over clientWidth on every cell, every Kubernetes tab, 600 to 1440 wide, screen-reader text excluded): clipped cells fall from 566 to 356 against main with no column worse at any width; Deployment, Resource and Namespace clips on Workloads and Configuration go from 25 to 0, Scope from 136 to 7, Age from 22 to 1", + "Workloads at 700: every deployment and namespace reads in full (main showed ingress-nginx-cont... and monitori...); columns are Deployment, Namespace, Updated, Ready, Available, Age with Ready reading 2/2; at 1300 and 1440 Cluster and Desired return and Ready reads 2", + "Configuration at 700: ConfigMap, Secret and RBAC names read in full (main cut them to checkout-api-runti... and payments-worker-...), Scope shows the namespace (apps, monitoring, services, Cluster) instead of Production EU/ser, and long Lifecycle and Data shape values end in an ellipsis inside their own cell where main spilled 1 subject - Service past the card edge", + "Row text alignment on every Kubernetes tab at 700 and 1440: each row's cell text sits on one line (0px spread); main's Scope and Data shape spans sat about 4px above the row", + "All 205 block truncate summary spans are the only content of their cell and sit in fixed-layout tables", + "Phone at 390: Deployments Age reads 18h ago (main showed 18h a...), Ready keeps 2/2; Configuration shows the namespace scope and ellipsizes Data inside the cell; at 360 Age stays hidden as before", + "Scope cell exposes the full cluster/namespace scope to assistive technology through a screen-reader span and keeps it on the title" + ], + "interactions": [ + "Ran the clip sweep on main (reverse-applied patch) and on the change across seven widths", + "Tuned tablet-band widths live by injecting --platform-table-band-width at 600, 650, 700 and 767", + "Measured row text alignment and block-span containment in the live DOM", + "Captured before/after table screenshots at 700 and 390 for Workloads and Configuration" + ] +} diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index a56a8d8ddc..e9d9cc865b 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -274,6 +274,20 @@ describe('App architecture', () => { expect(appStylesSource).not.toContain('white-space: normal;\n -webkit-box-orient'); }); + it('keeps the desired replica count on screen in the Deployments table', () => { + // Desired returns with the 72rem tier, so Ready's own /desired total hides + // only there, after its base rule so the container rule wins. + const base = appStylesSource.indexOf('.k8s-deployment-ready-total {\n display: inline;'); + const hide = appStylesSource.indexOf( + '@container (min-width: 72rem) {\n .k8s-deployment-ready-total {\n display: none;', + ); + expect(base).toBeGreaterThan(-1); + expect(hide).toBeGreaterThan(base); + expect(appStylesSource.match(/\.k8s-deployment-ready-total \{\s*display: none;/g)).toHaveLength( + 1, + ); + }); + it('keeps the identity anchor between the phone projection and the md column widths', () => { // Column visibility is container-led but md:w-[...] widths are viewport // media queries, so a 544-767px band showed every column with no widths and @@ -283,6 +297,11 @@ describe('App architecture', () => { expect(band).toMatch( /^@media \(width < 768px\) \{\s*@container \(min-width: 34rem\) \{\s*\.table-scroll-shell > \.table-fixed\.platform-table th\.platform-table-name-column \{\s*width: 30%;/, ); + // A table that needs unequal room in the band declares each header's + // width, and the shell applies it only there (after the name anchor). + expect(band).toMatch( + /width: 30%;\s*\}[\s\S]*?th\[style\*='--platform-table-band-width'\] \{\s*width: var\(--platform-table-band-width\);/, + ); }); it('keeps compact phone settings spacing scoped to the settings content shell', () => { diff --git a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx index 9f92539052..5713ea2d46 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx @@ -32,7 +32,9 @@ import { filterKubernetesResources, kubernetesScopeLabel, type KubernetesResourceStatusFilter, + isSingleKubernetesCluster, } from './kubernetesPageModel'; +import { KubernetesScopeText } from './KubernetesScopeText'; const autoscalerName = (resource: Resource): string => asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id; @@ -105,6 +107,7 @@ export const KubernetesAutoscalingTable: Component<{ externalSearch?: () => string; externalStatus?: () => KubernetesResourceStatusFilter; }> = (props) => { + const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources)); const tableState = createPlatformTableFilterState({ resources: () => props.resources, initialStatus: 'all' as KubernetesResourceStatusFilter, @@ -237,7 +240,6 @@ export const KubernetesAutoscalingTable: Component<{ {(resource) => { const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => autoscalerName(resource); - const scope = () => kubernetesScopeLabel(resource); const target = () => targetRef(resource); const metrics = () => metricSources(resource); const labels = () => labelSummary(resource); @@ -273,14 +275,16 @@ export const KubernetesAutoscalingTable: Component<{ - + {target()} @@ -306,14 +310,14 @@ export const KubernetesAutoscalingTable: Component<{ diff --git a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx index c1424aff0f..a4cea17020 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx @@ -31,7 +31,9 @@ import { filterKubernetesResources, kubernetesScopeLabel, type KubernetesResourceStatusFilter, + isSingleKubernetesCluster, } from './kubernetesPageModel'; +import { KubernetesScopeText } from './KubernetesScopeText'; // ConfigMaps and Secrets are intentionally rendered as API metadata. Pulse may // know that keys exist, but metadata-only collection must never imply that @@ -198,6 +200,7 @@ export const KubernetesConfigTable: Component<{ externalSearch?: () => string; externalStatus?: () => KubernetesResourceStatusFilter; }> = (props) => { + const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources)); const tableState = createPlatformTableFilterState({ resources: () => props.resources, initialStatus: 'all' as KubernetesResourceStatusFilter, @@ -264,7 +267,8 @@ export const KubernetesConfigTable: Component<{ kind="name" sort={sort} sortKey="resource" - class="platform-table-mobile-w-30 md:w-[22%]" + class="platform-table-mobile-w-30 md:w-[27%]" + bandWidth={35} > Resource @@ -272,7 +276,8 @@ export const KubernetesConfigTable: Component<{ kind="text" sort={sort} sortKey="kind" - class="platform-table-mobile-w-15 md:w-[13%]" + class="platform-table-mobile-w-15 md:w-[15%]" + bandWidth={16} > Kind @@ -280,7 +285,8 @@ export const KubernetesConfigTable: Component<{ kind="text" sort={sort} sortKey="scope" - class="platform-table-mobile-w-20 md:w-[16%]" + class="platform-table-mobile-w-20 md:w-[10%]" + bandWidth={14} > Scope @@ -288,7 +294,8 @@ export const KubernetesConfigTable: Component<{ kind="text" sort={sort} sortKey="lifecycle" - class="platform-table-phone-hidden md:w-[16%]" + class="platform-table-phone-hidden md:w-[17%]" + bandWidth={16} > @@ -296,6 +303,7 @@ export const KubernetesConfigTable: Component<{ kind="text" sort={sort} class="platform-table-mobile-w-15 md:w-[14%]" + bandWidth={19} > @@ -322,7 +330,6 @@ export const KubernetesConfigTable: Component<{ const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => configName(resource); const kind = () => configKind(resource); - const scope = () => kubernetesScopeLabel(resource); const state = () => lifecycleOrTrust(resource); const data = () => dataShape(resource); const refs = () => serviceAccountRefs(resource); @@ -364,9 +371,11 @@ export const KubernetesConfigTable: Component<{ - - {scope()} - + - + {data().label} diff --git a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx index 00d3c6da90..a70172c5b5 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx @@ -36,7 +36,9 @@ import { kubernetesScopeLabel, mapKubernetesControllerStatus, type KubernetesResourceStatusFilter, + isSingleKubernetesCluster, } from './kubernetesPageModel'; +import { KubernetesScopeText } from './KubernetesScopeText'; import { KubernetesNameText } from './KubernetesNameText'; const controllerName = (resource: Resource): string => @@ -243,6 +245,7 @@ export const KubernetesControllersTable: Component<{ externalSearch?: () => string; externalStatus?: () => KubernetesResourceStatusFilter; }> = (props) => { + const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources)); const tableState = createPlatformTableFilterState({ resources: () => props.resources, initialStatus: 'all' as KubernetesResourceStatusFilter, @@ -397,7 +400,6 @@ export const KubernetesControllersTable: Component<{ const indicator = () => mapKubernetesControllerStatus(resource); const name = () => controllerName(resource); const kind = () => controllerKind(resource); - const scope = () => kubernetesScopeLabel(resource); const target = () => targetValue(resource); const exceptions = () => exceptionSummary(resource); const detail = () => apiDetail(resource); @@ -439,14 +441,16 @@ export const KubernetesControllersTable: Component<{ - + {target()} @@ -468,7 +472,7 @@ export const KubernetesControllersTable: Component<{ - + {exceptions()} diff --git a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx index 5407855cac..44baa92736 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx @@ -174,13 +174,18 @@ export const KubernetesDeploymentsTable: Component<{ The integer-count columns (Desired / Updated / Ready / Available) trim to what their headers plus 1-2 digit values need. The basic tier restores namespace context - without crowding the phone tier. + without crowding the phone tier. Desired is the + denominator Ready already shows (2/2), so it waits for a + wide table. bandWidth sizes the tablet band below the + md widths, where a long deployment name needs about a + third of the row and the counts need little. */} Deployment @@ -188,7 +193,8 @@ export const KubernetesDeploymentsTable: Component<{ kind="text" sort={sort} sortKey="namespace" - class="platform-table-mobile-w-15 md:w-[20%]" + class="platform-table-mobile-w-15 md:w-[18%]" + bandWidth={16} > Namespace @@ -204,7 +210,7 @@ export const KubernetesDeploymentsTable: Component<{ kind="numeric-value" sort={sort} sortKey="desired" - class="platform-table-phone-hidden md:w-[8%]" + class="hidden md:table-cell md:w-[8%]" > @@ -213,6 +219,7 @@ export const KubernetesDeploymentsTable: Component<{ sort={sort} sortKey="updated" class="platform-table-phone-hidden md:w-[8%]" + bandWidth={12} > @@ -220,7 +227,8 @@ export const KubernetesDeploymentsTable: Component<{ kind="numeric-value" sort={sort} sortKey="ready" - class="platform-table-mobile-w-15 md:w-[7%]" + class="platform-table-mobile-w-10 md:w-[7%]" + bandWidth={9.5} > @@ -229,6 +237,7 @@ export const KubernetesDeploymentsTable: Component<{ sort={sort} sortKey="available" class="platform-table-phone-hidden md:w-[9%]" + bandWidth={14} > @@ -236,7 +245,8 @@ export const KubernetesDeploymentsTable: Component<{ kind="numeric-value" sort={sort} sortKey="age" - class="platform-table-mobile-w-10 md:w-[6%] platform-table-narrow-hidden" + class="platform-table-mobile-w-15 md:w-[8%] platform-table-narrow-hidden" + bandWidth={12.5} > Age @@ -292,7 +302,7 @@ export const KubernetesDeploymentsTable: Component<{ {cluster()}