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<{
-
- {scope()}
-
+
-
+
{target()}
@@ -306,14 +310,14 @@ export const KubernetesAutoscalingTable: Component<{
-
+
{metrics().label}
-
+
{labels().label}
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}
-
+
{refs().label}
-
+
{labels().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<{
-
- {scope()}
-
+
-
+
{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()}
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
@@ -59,6 +60,7 @@ export const KubernetesEventsTable: Component<{
showToolbar?: boolean;
}> = (props) => {
const sortedEvents = createMemo(() => [...props.resources].sort(compareKubernetesEvents));
+ const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
const tableState = createPlatformTableFilterState({
resources: sortedEvents,
initialStatus: 'all' as KubernetesResourceStatusFilter,
@@ -161,7 +163,6 @@ export const KubernetesEventsTable: Component<{
const indicator = () =>
mapKubernetesEventSeverity(resource.kubernetes?.eventType);
const name = () => eventName(resource);
- const scope = () => kubernetesScopeLabel(resource);
const observed = () => observedTimestamp(resource);
const message = () =>
formatPlatformTableTextValue(resource.kubernetes?.message);
@@ -197,9 +198,11 @@ export const KubernetesEventsTable: Component<{
-
- {scope()}
-
+
{formatPlatformTableTextValue(resource.kubernetes?.reason)}
@@ -219,10 +222,7 @@ export const KubernetesEventsTable: Component<{
-
+
{involvedObject(resource)}
@@ -234,14 +234,14 @@ export const KubernetesEventsTable: Component<{
-
+
-
+
{message()}
diff --git a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx
index 547eae2e74..d1e76f62ed 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx
@@ -32,7 +32,9 @@ import {
filterKubernetesResources,
kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
+ isSingleKubernetesCluster,
} from './kubernetesPageModel';
+import { KubernetesScopeText } from './KubernetesScopeText';
const resourceName = (resource: Resource): string =>
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
@@ -142,6 +144,7 @@ export const KubernetesNetworkingTable: Component<{
externalSearch?: () => string;
externalStatus?: () => KubernetesResourceStatusFilter;
}> = (props) => {
+ const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
@@ -265,7 +268,6 @@ export const KubernetesNetworkingTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => resourceName(resource);
- const scope = () => kubernetesScopeLabel(resource);
const address = () => addressOrHosts(resource);
const ports = () => portLabel(resource);
const targets = () => targetSummary(resource);
@@ -309,9 +311,11 @@ export const KubernetesNetworkingTable: Component<{
-
- {scope()}
-
+
-
+
{address().label}
-
+
{ports().label}
-
+
{targets().label}
diff --git a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx
index 1c0a4c24bf..f752960f08 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx
@@ -427,7 +427,7 @@ export const KubernetesNodesTable: Component<{
-
+
{runtime()}
diff --git a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
index 9bdecea7d9..7e1cd2fe9e 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
@@ -32,7 +32,9 @@ import {
kubernetesScopeLabel,
mapKubernetesPodStatus,
type KubernetesResourceStatusFilter,
+ isSingleKubernetesCluster,
} from './kubernetesPageModel';
+import { KubernetesScopeText } from './KubernetesScopeText';
import { KubernetesNameText } from './KubernetesNameText';
const podName = (resource: Resource): string =>
@@ -143,6 +145,7 @@ export const KubernetesPodsTable: Component<{
externalSearch?: () => string;
externalStatus?: () => KubernetesResourceStatusFilter;
}> = (props) => {
+ const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
@@ -292,7 +295,6 @@ export const KubernetesPodsTable: Component<{
{(resource) => {
const indicator = () => mapKubernetesPodStatus(resource);
const name = () => podName(resource);
- const scope = () => kubernetesScopeLabel(resource);
const node = () => formatPlatformTableTextValue(resource.kubernetes?.nodeName);
const owner = () => ownerValue(resource);
const image = () => imageValue(resource);
@@ -334,9 +336,11 @@ export const KubernetesPodsTable: Component<{
-
- {scope()}
-
+
string;
externalStatus?: () => KubernetesResourceStatusFilter;
}> = (props) => {
+ const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
@@ -247,26 +250,31 @@ export const KubernetesPolicyTable: Component<{
<>
Resource
Kind
Scope
@@ -288,7 +296,6 @@ export const KubernetesPolicyTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => policyName(resource);
- const scope = () => kubernetesScopeLabel(resource);
const shape = () => policyShape(resource);
const spec = () => policySpec(resource);
const state = () => policyState(resource);
@@ -330,35 +337,37 @@ export const KubernetesPolicyTable: Component<{
-
- {scope()}
-
+
-
+
{shape().label}
-
+
{spec().label}
-
+
{state().label}
-
+
{labels().label}
diff --git a/frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx b/frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx
new file mode 100644
index 0000000000..129f194fb8
--- /dev/null
+++ b/frontend-modern/src/features/kubernetes/KubernetesScopeText.tsx
@@ -0,0 +1,28 @@
+import { Show, type Component } from 'solid-js';
+import type { Resource } from '@/types/resource';
+import { kubernetesScopeDisplayLabel, kubernetesScopeLabel } from './kubernetesPageModel';
+
+/**
+ * A Kubernetes scope cell. With one cluster in view it shows the namespace
+ * alone (the cluster name would repeat on every row), keeps the full
+ * cluster/namespace scope on the title, and gives assistive technology the full
+ * scope in place of the shortened text.
+ */
+export const KubernetesScopeText: Component<{
+ resource: Resource;
+ singleCluster: boolean;
+ class?: string;
+}> = (props) => {
+ const full = () => kubernetesScopeLabel(props.resource);
+ const shown = () => kubernetesScopeDisplayLabel(props.resource, props.singleCluster);
+ return (
+
+ {shown()}
+
+ {full()}
+
+
+ );
+};
+
+export default KubernetesScopeText;
diff --git a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx
index 60777875d3..3c376e93de 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx
@@ -32,7 +32,9 @@ import {
filterKubernetesResources,
kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
+ isSingleKubernetesCluster,
} from './kubernetesPageModel';
+import { KubernetesScopeText } from './KubernetesScopeText';
// Services have a distinct API contract from Ingress and EndpointSlice:
// operators need the Service type, virtual IP, exposed ports, node ports, and
@@ -100,6 +102,7 @@ export const KubernetesServicesTable: Component<{
externalSearch?: () => string;
externalStatus?: () => KubernetesResourceStatusFilter;
}> = (props) => {
+ const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
@@ -223,7 +226,6 @@ export const KubernetesServicesTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => serviceName(resource);
- const scope = () => kubernetesScopeLabel(resource);
const serviceType = () =>
formatPlatformTableTextValue(resource.kubernetes?.serviceType);
const clusterIp = () =>
@@ -266,9 +268,11 @@ export const KubernetesServicesTable: Component<{
-
- {scope()}
-
+
-
+
{externalIps().label}
-
+
{ports().label}
-
+
{selector().label}
diff --git a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx
index 4955ce4b99..f45622e555 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx
@@ -31,7 +31,9 @@ import {
filterKubernetesResources,
kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
+ isSingleKubernetesCluster,
} from './kubernetesPageModel';
+import { KubernetesScopeText } from './KubernetesScopeText';
// Kubernetes storage is intentionally not rendered by the generic
// inventory table. StorageClass, PersistentVolume, and
@@ -213,6 +215,7 @@ export const KubernetesStorageTable: Component<{
title?: string;
showToolbar?: boolean;
}> = (props) => {
+ const singleCluster = createMemo(() => isSingleKubernetesCluster(props.resources));
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
initialStatus: 'all' as KubernetesResourceStatusFilter,
@@ -346,7 +349,6 @@ export const KubernetesStorageTable: Component<{
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => storageName(resource);
const kind = () => storageKind(resource);
- const scope = () => kubernetesScopeLabel(resource);
const policy = () => policyLabel(resource);
const target = () => bindingTarget(resource);
const detailRowId = () => drawer.detailRowId(resource);
@@ -386,9 +388,11 @@ export const KubernetesStorageTable: Component<{
-
- {scope()}
-
+
-
+
{policy().label}
-
+
{target().label}
diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesAutoscalingTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesAutoscalingTable.test.tsx
index 3102dc8bf8..11c70864cb 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesAutoscalingTable.test.tsx
+++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesAutoscalingTable.test.tsx
@@ -61,7 +61,10 @@ describe('KubernetesAutoscalingTable', () => {
expect(screen.getByText('Bounds')).toBeInTheDocument();
expect(screen.getByText('Metrics')).toBeInTheDocument();
expect(screen.getByText('checkout-api-hpa')).toBeInTheDocument();
- expect(screen.getByText('prod/apps')).toBeInTheDocument();
+ expect(screen.getByText('apps').closest('[data-kubernetes-scope]')).toHaveAttribute(
+ 'title',
+ 'prod/apps',
+ );
expect(screen.getByText('Deployment/checkout-api')).toBeInTheDocument();
expect(screen.getByText('2-10')).toBeInTheDocument();
expect(screen.getByText('4')).toBeInTheDocument();
diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx
index 9be6ae02b0..fdbb27a973 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx
+++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx
@@ -124,4 +124,39 @@ describe('KubernetesDeploymentsTable', () => {
expect(screen.getByTitle('1 / 3 ready')).toHaveClass('bg-amber-500');
expect(screen.getByTitle('Ready')).toHaveClass('bg-emerald-500');
});
+
+ it('sizes the tablet band for a long name and keeps Desired for wide tables', () => {
+ const { container } = render(() => (
+ }
+ emptyTitle="No deployments"
+ emptyDescription="No deployments"
+ showToolbar={false}
+ />
+ ));
+
+ const headers = [...container.querySelectorAll('thead th')];
+ const byLabel = (label: string) =>
+ headers.find((th) => th.textContent?.includes(label)) as HTMLElement | undefined;
+ // The band gives the name about a third of the row and the counts little.
+ expect(byLabel('Deployment')?.style.getPropertyValue('--platform-table-band-width')).toBe(
+ '36%',
+ );
+ expect(byLabel('Ready')?.style.getPropertyValue('--platform-table-band-width')).toBe('9.5%');
+ // Ready already shows the desired count as its denominator (2/2).
+ const desired = byLabel('Desired');
+ expect(desired).toHaveClass('hidden', 'md:table-cell');
+ expect(desired?.style.getPropertyValue('--platform-table-band-width')).toBe('');
+ });
});
diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesPodsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesPodsTable.test.tsx
index 8969c88605..8e6ebfbde4 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesPodsTable.test.tsx
+++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesPodsTable.test.tsx
@@ -112,7 +112,13 @@ describe('KubernetesPodsTable', () => {
expect(screen.getByText('Age')).toBeInTheDocument();
expect(screen.getByText(kubernetesName('checkout-api-6c746d5bcf-c7z2p'))).toBeInTheDocument();
- expect(screen.getByText('prod-euw1/services')).toBeInTheDocument();
+ // One cluster in view: the scope cell shows the namespace and keeps the
+ // full cluster/namespace scope on hover.
+ const scopeCell = screen.getByText('services').closest('[data-kubernetes-scope]');
+ expect(scopeCell).toHaveAttribute('title', 'prod-euw1/services');
+ // Assistive technology gets the full scope in place of the shortened text.
+ expect(screen.getByText('services')).toHaveAttribute('aria-hidden', 'true');
+ expect(scopeCell?.querySelector('.sr-only')).toHaveTextContent('prod-euw1/services');
expect(screen.getByText(kubernetesName('prod-euw1-k8s-02'))).toBeInTheDocument();
// Narrow columns truncate the head and keep the generated tail, so
// replicas and nodes stay distinguishable. The title keeps the full name.
@@ -199,4 +205,28 @@ describe('KubernetesPodsTable', () => {
screen.getAllByTitle('Running').some((el) => el.classList.contains('bg-emerald-500')),
).toBe(true);
});
+
+ it('keeps the cluster in the scope cell when several clusters are in view', () => {
+ render(() => (
+ }
+ emptyTitle="No pods"
+ emptyDescription="No pods"
+ showToolbar={false}
+ />
+ ));
+
+ expect(screen.getByText('prod-eu/services')).toBeInTheDocument();
+ expect(screen.getByText('prod-us/services')).toBeInTheDocument();
+ });
});
diff --git a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
index c6b96ecd3d..8511c16e62 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
+++ b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
@@ -3,6 +3,8 @@ import type { Resource } from '@/types/resource';
import {
getKubernetesNodesNeedingAttention,
splitKubernetesNameTail,
+ isSingleKubernetesCluster,
+ kubernetesScopeDisplayLabel,
} from '../kubernetesPageModel';
import {
KUBERNETES_TAB_SPECS,
@@ -47,6 +49,52 @@ const makeResource = (resource: Partial & Pick {
+ it('shows the namespace alone when one cluster is in view', () => {
+ const inCluster = (id: string, clusterName: string, namespace?: string) =>
+ ({
+ id,
+ type: 'pod',
+ name: id,
+ kubernetes: { clusterName, ...(namespace ? { namespace } : {}) },
+ }) as unknown as Resource;
+ const services = inCluster('a', 'Production EU', 'services');
+ const clusterRole = inCluster('b', 'Production EU');
+ const otherCluster = inCluster('c', 'Staging', 'services');
+
+ expect(isSingleKubernetesCluster([services, clusterRole])).toBe(true);
+ expect(isSingleKubernetesCluster([services, otherCluster])).toBe(false);
+ expect(isSingleKubernetesCluster([])).toBe(true);
+ // Two clusters can share a display name, and a row with no cluster at all
+ // counts as its own, so neither case loses the cluster from the cell.
+ const sameNameOtherId = {
+ id: 'd',
+ type: 'pod',
+ name: 'd',
+ kubernetes: { clusterId: 'eu-2', clusterName: 'Production EU', namespace: 'services' },
+ } as unknown as Resource;
+ const euOne = {
+ id: 'e',
+ type: 'pod',
+ name: 'e',
+ kubernetes: { clusterId: 'eu-1', clusterName: 'Production EU', namespace: 'apps' },
+ } as unknown as Resource;
+ const noCluster = {
+ id: 'f',
+ type: 'pod',
+ name: 'f',
+ kubernetes: { namespace: 'apps' },
+ } as unknown as Resource;
+ expect(isSingleKubernetesCluster([euOne, sameNameOtherId])).toBe(false);
+ expect(isSingleKubernetesCluster([euOne, noCluster])).toBe(false);
+ expect(isSingleKubernetesCluster([noCluster])).toBe(true);
+
+ expect(kubernetesScopeDisplayLabel(services, true)).toBe('services');
+ expect(kubernetesScopeDisplayLabel(clusterRole, true)).toBe('Cluster');
+ // Several clusters keep the cluster in the cell so rows stay unambiguous.
+ expect(kubernetesScopeDisplayLabel(services, false)).toBe('Production EU/services');
+ expect(kubernetesScopeDisplayLabel(clusterRole, false)).toBe('Production EU');
+ });
+
it('splits off the generated tail that tells sibling names apart', () => {
expect(splitKubernetesNameTail('checkout-api-6d8f9c7b5-x7k2p')).toEqual({
head: 'checkout-api-6d8f9c7b5',
diff --git a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
index e8e21d864b..14cb794a50 100644
--- a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
+++ b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
@@ -137,6 +137,25 @@ const podHasFatalContainer = (containers: ResourceKubernetesPodContainerStatus[]
const podAllContainersReady = (containers: ResourceKubernetesPodContainerStatus[]): boolean =>
containers.length > 0 && containers.every((container) => container.ready === true);
+// With one cluster in view, every row's scope repeats the cluster name and
+// crowds out the namespace, the part that differs between rows. Tables then
+// show the namespace alone (Cluster-wide for cluster-scoped objects) and keep
+// the full cluster/namespace scope on hover. Several clusters keep both.
+// Clusters are told apart by ID (two can share a display name), and a row with
+// no cluster at all counts as its own, so a mix keeps the full scope.
+const kubernetesClusterKey = (resource: Resource): string =>
+ asTrimmedString(resource.kubernetes?.clusterId) ||
+ asTrimmedString(resource.kubernetes?.clusterName) ||
+ '';
+
+export const isSingleKubernetesCluster = (resources: readonly Resource[]): boolean =>
+ new Set(resources.map(kubernetesClusterKey)).size <= 1;
+
+export const kubernetesScopeDisplayLabel = (resource: Resource, singleCluster: boolean): string =>
+ singleCluster
+ ? asTrimmedString(resource.kubernetes?.namespace) || 'Cluster'
+ : kubernetesScopeLabel(resource);
+
export function mapKubernetesPodStatus(resource: Resource): StatusIndicator {
const phase = normalizeKubernetesToken(
resource.kubernetes?.podPhase || resource.kubernetes?.phase,
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 665d4429cb..22a4c5545c 100644
--- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts
+++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts
@@ -370,7 +370,7 @@ describe('platform overview layout guardrails', () => {
/sortKey="capacity"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[16%\]"/,
);
expect(kubernetesDeploymentsTableSource).toMatch(
- /sortKey="namespace"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[20%\]"/,
+ /sortKey="namespace"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[18%\]"/,
);
expect(kubernetesPodsTableSource).toMatch(
/sortKey="scope"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[13%\]"/,
@@ -388,13 +388,34 @@ describe('platform overview layout guardrails', () => {
/sortKey="class"[\s\S]{0,120}?class="platform-table-mobile-w-20 md:w-\[10%\]"/,
);
expect(kubernetesConfigTableSource).toMatch(
- /sortKey="scope"[\s\S]{0,120}?class="platform-table-mobile-w-20 md:w-\[16%\]"/,
+ /sortKey="scope"[\s\S]{0,120}?class="platform-table-mobile-w-20 md:w-\[10%\]"/,
);
expect(kubernetesPolicyTableSource).toContain(
- "getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[15%]",
+ "getPlatformTableHeadClassForKind('text')} platform-table-mobile-w-15 md:w-[12%]",
);
});
+ it('keeps Kubernetes 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 [
+ kubernetesAutoscalingTableSource,
+ kubernetesConfigTableSource,
+ kubernetesControllersTableSource,
+ kubernetesEventsTableSource,
+ kubernetesNetworkingTableSource,
+ kubernetesNodesTableSource,
+ kubernetesPolicyTableSource,
+ kubernetesServicesTableSource,
+ kubernetesStorageTableSource,
+ ]) {
+ expect(source).not.toMatch(/class="[^"]*\binline-block\b[^"]*\btruncate\b/);
+ expect(source).not.toMatch(/class="[^"]*\btruncate\b[^"]*\binline-block\b/);
+ expect(source).toContain('class="block truncate"');
+ }
+ });
+
it('keeps phone-priority visibility container-led and symmetric across table rows', () => {
expect(indexCssSource).toContain(':is(th, td).platform-table-phone-hidden');
expect(indexCssSource).toContain(':is(th, td).platform-table-phone-only');
@@ -417,8 +438,17 @@ describe('platform overview layout guardrails', () => {
expect(dockerVolumesTableSource).toMatch(
/getPlatformTableCellClassForKind\('text'\)[\s\S]{0,100}?platform-table-phone-hidden[\s\S]{0,140}?dockerTextValue\(resource\.docker\?\.scope\)/,
);
+ // Desired is the denominator Ready already shows (2/2), so it waits for a
+ // wide table rather than returning with the tablet tier.
expect(kubernetesDeploymentsTableSource).toMatch(
- /sortKey="desired"[\s\S]{0,120}?platform-table-phone-hidden/,
+ /sortKey="desired"[\s\S]{0,120}?class="hidden md:table-cell md:w-\[8%\]"/,
+ );
+ // On a phone Ready only shows 2/2 while Age needs room for "18h ago".
+ expect(kubernetesDeploymentsTableSource).toMatch(
+ /sortKey="ready"[\s\S]{0,120}?class="platform-table-mobile-w-10 md:w-\[7%\]"/,
+ );
+ expect(kubernetesDeploymentsTableSource).toMatch(
+ /sortKey="age"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[8%\] platform-table-narrow-hidden"/,
);
expect(truenasVirtualMachinesTableSource).toMatch(
/sortKey="state"[\s\S]{0,120}?platform-table-phone-hidden/,
diff --git a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts
index dff413c449..d7b2822f11 100644
--- a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts
+++ b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.test.ts
@@ -37,6 +37,7 @@ import {
summarizePlatformTableValues,
withPlatformStatusCounts,
type PlatformResourceStatusFilter,
+ platformTableBandWidthStyle,
} from '../sharedPlatformPage';
afterEach(() => {
@@ -917,4 +918,10 @@ describe('PlatformTableMetricFallback', () => {
expect(marker.getAttribute('title')).toBe('Update this agent for full machine telemetry.');
expect(marker.getAttribute('aria-hidden')).toBeNull();
});
+
+ it('declares tablet band widths as a custom property the shell reads', () => {
+ expect(platformTableBandWidthStyle(36)).toEqual({ '--platform-table-band-width': '36%' });
+ expect(platformTableBandWidthStyle(9.5)).toEqual({ '--platform-table-band-width': '9.5%' });
+ expect(platformTableBandWidthStyle(undefined)).toBeUndefined();
+ });
});
diff --git a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx
index d7dc45c1ff..2244444b24 100644
--- a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx
+++ b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx
@@ -590,12 +590,23 @@ export function createPlatformTableSortState(options: {
return { sortKey, sortDirection, handleSort, getAriaSort, sortRows };
}
+// The tablet band between the phone projection and the md column widths (a
+// 34rem container below a 768px viewport) applies no md:w-[...] width, so a
+// table whose columns need unequal room there declares each header's percent
+// width through this style, which the shared shell reads only in that band.
+export const platformTableBandWidthStyle = (
+ percent: number | undefined,
+): JSX.CSSProperties | undefined =>
+ percent === undefined ? undefined : { '--platform-table-band-width': `${percent}%` };
+
export function PlatformSortableTableHead(props: {
kind: PlatformTableColumnKind;
sort: PlatformTableSortState;
// Omit to render a non-sortable header with the same canonical alignment.
sortKey?: SortKey;
class?: string;
+ // Percent width for the tablet band (see platformTableBandWidthStyle).
+ bandWidth?: number;
children: JSX.Element;
}) {
const isSorted = () => props.sortKey !== undefined && props.sort.sortKey() === props.sortKey;
@@ -611,6 +622,7 @@ export function PlatformSortableTableHead(props: {
.replace(/\s+/g, ' ')
.trim()}
aria-sort={props.sortKey !== undefined ? props.sort.getAriaSort(props.sortKey) : undefined}
+ style={platformTableBandWidthStyle(props.bandWidth)}
onClick={handleClick}
>
{props.children}
diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css
index 240aea953e..e2b4d75377 100644
--- a/frontend-modern/src/index.css
+++ b/frontend-modern/src/index.css
@@ -529,6 +529,15 @@
display: inline;
}
+ /* The Deployments Desired column returns with the 72rem tier (hidden
+ md:table-cell), so Ready drops its own /desired total there and never
+ before: one of the two always carries the desired count. */
+ @container (min-width: 72rem) {
+ .k8s-deployment-ready-total {
+ display: none;
+ }
+ }
+
@container (min-width: 50rem) {
.table-scroll-shell col.alert-history-context-track {
visibility: visible;
@@ -568,10 +577,6 @@
.table-scroll-shell :is(th, td).storage-dataset-referenced-column {
display: table-cell;
}
-
- .k8s-deployment-ready-total {
- display: none;
- }
}
@container (min-width: 64rem) {
@@ -1446,6 +1451,13 @@
.table-scroll-shell > .table-fixed.platform-table th.platform-table-name-column {
width: 30%;
}
+
+ /* A table whose columns need unequal room in this band (short counts
+ beside a long name) declares each width through the header's
+ bandWidth, which the shell applies only here. */
+ .table-scroll-shell > .table-fixed.platform-table th[style*='--platform-table-band-width'] {
+ width: var(--platform-table-band-width);
+ }
}
}