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);
+ }
}
}