diff --git a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx index ddc6a0691b..91256b45aa 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx @@ -32,6 +32,7 @@ import { import type { Resource } from '@/types/resource'; import { filterKubernetesResources, + kubernetesScopeLabel, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -40,15 +41,6 @@ const textValue = (value: string | undefined): string => asTrimmedString(value) const autoscalerName = (resource: Resource): string => asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id; -const autoscalerScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const targetRef = (resource: Resource): string => textValue( [resource.kubernetes?.targetKind, resource.kubernetes?.targetName].filter(Boolean).join('/'), @@ -184,7 +176,7 @@ export const KubernetesAutoscalingTable: Component<{ {(resource) => { const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => autoscalerName(resource); - const scope = () => autoscalerScope(resource); + const scope = () => kubernetesScopeLabel(resource); const target = () => targetRef(resource); const metrics = () => metricSources(resource); const labels = () => labelSummary(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx index c14ecfde57..c928319b5b 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx @@ -32,6 +32,7 @@ import { import type { Resource } from '@/types/resource'; import { filterKubernetesResources, + kubernetesScopeLabel, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -56,15 +57,6 @@ const configKind = (resource: Resource): string => { return resource.kubernetes?.resourceKind || resource.type; }; -const configScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const plural = (count: number, singular: string, pluralLabel = `${singular}s`): string => `${count} ${count === 1 ? singular : pluralLabel}`; @@ -284,7 +276,7 @@ export const KubernetesConfigTable: Component<{ const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => configName(resource); const kind = () => configKind(resource); - const scope = () => configScope(resource); + const scope = () => kubernetesScopeLabel(resource); const state = () => lifecycleOrTrust(resource); const data = () => dataShape(resource); const refs = () => serviceAccountRefs(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx index 05f89acd89..1cf52d4f22 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx @@ -33,6 +33,7 @@ import type { Resource } from '@/types/resource'; import { compareKubernetesControllers, filterKubernetesResources, + kubernetesScopeLabel, mapKubernetesControllerStatus, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -48,15 +49,6 @@ const controllerName = (resource: Resource): string => const controllerKind = (resource: Resource): string => resource.kubernetes?.resourceKind || getResourceTypeLabel(resource.type) || resource.type; -const controllerScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const targetValue = (resource: Resource): string => { switch (resource.type) { case 'k8s-replicaset': @@ -278,7 +270,7 @@ export const KubernetesControllersTable: Component<{ const indicator = () => mapKubernetesControllerStatus(resource); const name = () => controllerName(resource); const kind = () => controllerKind(resource); - const scope = () => controllerScope(resource); + const scope = () => kubernetesScopeLabel(resource); const target = () => targetValue(resource); const exceptions = () => exceptionSummary(resource); const detail = () => apiDetail(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx index 4c99b8c666..151b3d7420 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx @@ -33,6 +33,7 @@ import type { Resource } from '@/types/resource'; import { compareKubernetesDeployments, filterKubernetesResources, + kubernetesClusterLabel, mapKubernetesDeploymentStatus, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -43,8 +44,9 @@ import { // render as dashes. This deployment-native table reuses canonical shared // primitives (Card, Table, SearchInput, FilterButtonGroup, StatusDot) but // surfaces deployment-meaningful columns only: namespace, cluster, -// desired / updated / ready / available replicas, observed generation, and -// metadata age. +// desired / updated / ready / available replicas, and metadata age. +// observedGeneration is deliberately not a column: without the spec +// generation beside it the raw number is unactionable. const replicaCount = (value: number | undefined): JSX.Element => ( {value ?? 0} @@ -121,19 +123,19 @@ export const KubernetesDeploymentsTable: Component<{ Desktop widths: Deployment, Namespace, and Cluster take the biggest shares because their content can be long. The integer-count columns (Desired / Updated / Ready / - Available / Observed) trim to what their headers plus - 1-2 digit values need. Mobile widths are unchanged. + Available) trim to what their headers plus 1-2 digit + values need. Mobile widths are unchanged. */} - + Deployment @@ -157,11 +159,6 @@ export const KubernetesDeploymentsTable: Component<{ > Available - @@ -174,10 +171,7 @@ export const KubernetesDeploymentsTable: Component<{ {(deployment) => { const name = () => asTrimmedString(deployment.name) || deployment.id; const ns = () => asTrimmedString(deployment.kubernetes?.namespace) || '—'; - const cluster = () => - asTrimmedString(deployment.kubernetes?.clusterName) || - asTrimmedString(deployment.kubernetes?.clusterId) || - '—'; + const cluster = () => kubernetesClusterLabel(deployment) || '—'; const indicator = () => mapKubernetesDeploymentStatus(deployment); const age = () => formatAge(deployment.kubernetes?.createdAt); const detailRowId = () => drawer.detailRowId(deployment); @@ -236,11 +230,6 @@ export const KubernetesDeploymentsTable: Component<{ > {replicaCount(deployment.kubernetes?.availableReplicas)} - @@ -253,7 +242,7 @@ export const KubernetesDeploymentsTable: Component<{ resource={deployment} open={isExpanded()} detailRowId={detailRowId()} - colSpan={9} + colSpan={8} resolveResourceLabel={resolveResourceLabel} onClose={() => drawer.close(deployment)} /> diff --git a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx index eaa6f62c1f..50845d9806 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx @@ -11,6 +11,7 @@ import { TableRow, } from '@/components/shared/Table'; import { asTrimmedString } from '@/utils/stringUtils'; +import { formatRelativeTime } from '@/utils/format'; import { PLATFORM_HEALTH_FILTER_OPTIONS, PLATFORM_TABLE_BODY_CLASS, @@ -32,6 +33,7 @@ import type { Resource } from '@/types/resource'; import { compareKubernetesEvents, filterKubernetesResources, + kubernetesScopeLabel, mapKubernetesEventSeverity, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -41,15 +43,6 @@ const textValue = (value: string | undefined): string => asTrimmedString(value) const eventName = (resource: Resource): string => asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id; -const eventScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const involvedObject = (resource: Resource): string => textValue( [resource.kubernetes?.involvedKind, resource.kubernetes?.involvedName] @@ -57,12 +50,16 @@ const involvedObject = (resource: Resource): string => .join('/'), ); -const observedTime = (resource: Resource): string => - textValue( +const observedTimestamp = (resource: Resource): string => + asTrimmedString( resource.kubernetes?.eventTime || resource.kubernetes?.firstSeen || resource.kubernetes?.createdAt, - ); + ) || ''; + +// Humanized for the cell ("2h ago"); the raw timestamp stays on the title. +const observedTime = (resource: Resource): string => + formatRelativeTime(observedTimestamp(resource), { compact: true, emptyText: '—' }) || '—'; const numberValue = (value: number | undefined): JSX.Element => typeof value === 'number' ? {value} : —; @@ -163,7 +160,7 @@ export const KubernetesEventsTable: Component<{ const indicator = () => mapKubernetesEventSeverity(resource.kubernetes?.eventType); const name = () => eventName(resource); - const scope = () => eventScope(resource); + const scope = () => kubernetesScopeLabel(resource); const observed = () => observedTime(resource); const message = () => textValue(resource.kubernetes?.message); const detailRowId = () => drawer.detailRowId(resource); @@ -233,7 +230,10 @@ export const KubernetesEventsTable: Component<{ diff --git a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx index 5a2a5a6a3e..532aadcaa3 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx @@ -32,6 +32,7 @@ import { import type { Resource } from '@/types/resource'; import { filterKubernetesResources, + kubernetesScopeLabel, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -46,15 +47,6 @@ const networkKind = (resource: Resource): string => { return resource.kubernetes?.resourceKind || resource.type; }; -const scopeLabel = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const summarizeValues = ( values: readonly (string | undefined)[] | undefined, visible = 2, @@ -226,7 +218,7 @@ export const KubernetesNetworkingTable: Component<{ {(resource) => { const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => resourceName(resource); - const scope = () => scopeLabel(resource); + const scope = () => kubernetesScopeLabel(resource); const address = () => addressOrHosts(resource); const ports = () => portLabel(resource); const targets = () => targetSummary(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx index 3778488cff..2c767278bb 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx @@ -35,6 +35,7 @@ import type { Resource } from '@/types/resource'; import { compareKubernetesNodes, filterKubernetesResources, + kubernetesClusterLabel, mapKubernetesNodeStatus, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -203,10 +204,7 @@ export const KubernetesNodesTable: Component<{ {(node) => { const meta = () => node.kubernetes; const name = () => asTrimmedString(node.name) || node.id; - const cluster = () => - asTrimmedString(meta()?.clusterName) || - asTrimmedString(meta()?.clusterId) || - '—'; + const cluster = () => kubernetesClusterLabel(node) || '—'; const kubelet = () => asTrimmedString(meta()?.kubeletVersion) || '—'; const runtime = () => asTrimmedString(meta()?.containerRuntimeVersion) || '—'; const capacityLabel = () => { diff --git a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx index 213196196d..e804b247a5 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx @@ -32,6 +32,7 @@ import type { Resource, ResourceKubernetesPodContainerStatus } from '@/types/res import { compareKubernetesPods, filterKubernetesResources, + kubernetesScopeLabel, mapKubernetesPodStatus, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -44,15 +45,6 @@ const podName = (resource: Resource): string => asTrimmedString(resource.name) || resource.id; -const podScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const podContainers = (resource: Resource): ResourceKubernetesPodContainerStatus[] => resource.kubernetes?.podContainers ?? []; @@ -202,7 +194,7 @@ export const KubernetesPodsTable: Component<{ {(resource) => { const indicator = () => mapKubernetesPodStatus(resource); const name = () => podName(resource); - const scope = () => podScope(resource); + const scope = () => kubernetesScopeLabel(resource); const node = () => textValue(resource.kubernetes?.nodeName); const owner = () => ownerValue(resource); const image = () => imageValue(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx index 7be5ae93a6..17f6223148 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx @@ -32,6 +32,7 @@ import { import type { Resource } from '@/types/resource'; import { filterKubernetesResources, + kubernetesScopeLabel, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -52,15 +53,6 @@ const policyKind = (resource: Resource): string => { return resource.kubernetes?.resourceKind || resource.type; }; -const policyScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const summarizeValues = ( values: readonly (string | undefined)[] | undefined, visible = 2, @@ -307,7 +299,7 @@ export const KubernetesPolicyTable: Component<{ {(resource) => { const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => policyName(resource); - const scope = () => policyScope(resource); + const scope = () => kubernetesScopeLabel(resource); const shape = () => policyShape(resource); const spec = () => policySpec(resource); const state = () => policyState(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx index a3be7cf582..cb658e9ebc 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx @@ -32,6 +32,7 @@ import { import type { Resource } from '@/types/resource'; import { filterKubernetesResources, + kubernetesScopeLabel, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -44,15 +45,6 @@ const textValue = (value: string | undefined): string => asTrimmedString(value) const serviceName = (resource: Resource): string => asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id; -const serviceScope = (resource: Resource): string => { - const cluster = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; - return cluster || 'Cluster'; -}; - const summarizeValues = ( values: readonly (string | undefined)[] | undefined, visible = 2, @@ -185,7 +177,7 @@ export const KubernetesServicesTable: Component<{ {(resource) => { const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => serviceName(resource); - const scope = () => serviceScope(resource); + const scope = () => kubernetesScopeLabel(resource); const serviceType = () => textValue(resource.kubernetes?.serviceType); const clusterIp = () => textValue(resource.kubernetes?.clusterIp); const externalIps = () => externalIpSummary(resource); diff --git a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx index 411127608f..5d920a62ab 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx @@ -33,6 +33,7 @@ import { import type { Resource } from '@/types/resource'; import { filterKubernetesResources, + kubernetesScopeLabel, type KubernetesResourceStatusFilter, } from './kubernetesPageModel'; @@ -62,17 +63,6 @@ const storageKind = (resource: Resource): string => { return resource.kubernetes?.resourceKind || resource.type; }; -const storageScope = (resource: Resource): string => { - const clusterScope = - asTrimmedString(resource.kubernetes?.clusterId) || - asTrimmedString(resource.kubernetes?.clusterName); - const namespace = asTrimmedString(resource.kubernetes?.namespace); - if (namespace) { - return clusterScope ? `${clusterScope}/${namespace}` : namespace; - } - return clusterScope || 'Cluster'; -}; - const bindingOrPhase = (resource: Resource): string => { if (resource.type === 'k8s-storage-class') { return textValue(resource.kubernetes?.volumeBindingMode || 'Immediate'); @@ -259,7 +249,7 @@ export const KubernetesStorageTable: Component<{ const indicator = () => getSimpleStatusIndicator(resource.status); const name = () => storageName(resource); const kind = () => storageKind(resource); - const scope = () => storageScope(resource); + const scope = () => kubernetesScopeLabel(resource); const policy = () => policyLabel(resource); const target = () => bindingTarget(resource); const detailRowId = () => drawer.detailRowId(resource); diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx index 11fc2b9857..ee844d536b 100644 --- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx +++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx @@ -66,7 +66,9 @@ describe('KubernetesDeploymentsTable', () => { expect(screen.getByText('Updated')).toBeInTheDocument(); expect(screen.getByText('Ready')).toBeInTheDocument(); expect(screen.getByText('Available')).toBeInTheDocument(); - expect(screen.getByText('Observed')).toBeInTheDocument(); + // observedGeneration is deliberately not a column: the raw number is + // unactionable without the spec generation beside it. + expect(screen.queryByText('Observed')).toBeNull(); expect(screen.getByText('Age')).toBeInTheDocument(); expect(screen.getByText('checkout-api')).toBeInTheDocument(); @@ -75,7 +77,7 @@ describe('KubernetesDeploymentsTable', () => { expect(screen.getByText('4')).toBeInTheDocument(); expect(screen.getByText('3')).toBeInTheDocument(); expect(screen.getAllByText('2')).toHaveLength(2); - expect(screen.getByText('12')).toBeInTheDocument(); + expect(screen.queryByText('12')).toBeNull(); expect(screen.getByText('2h ago')).toBeInTheDocument(); expect( document.querySelector('[data-kubernetes-deployment-row="checkout-api"]'), diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx index 42feb49e78..accb322e0a 100644 --- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx +++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx @@ -1,5 +1,5 @@ import { cleanup, render, screen } from '@solidjs/testing-library'; -import { afterEach, describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import type { Resource } from '@/types/resource'; import { KubernetesEventsTable } from '../KubernetesEventsTable'; @@ -24,10 +24,14 @@ const makeResource = ({ afterEach(() => { cleanup(); + vi.useRealTimers(); }); describe('KubernetesEventsTable', () => { it('renders Kubernetes Event fields with severity, newest-first ordering, and message context', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-05-24T15:00:00Z')); + render(() => ( { expect(screen.getByText('FailedScheduling')).toBeInTheDocument(); expect(screen.getByText('Pod/checkout-api-123')).toBeInTheDocument(); expect(screen.getByText('3')).toBeInTheDocument(); - expect(screen.getByText('2026-05-24T13:00:00Z')).toBeInTheDocument(); + // The cell humanizes the observed time; the raw timestamp stays on the + // hover title. + expect(screen.getByText('2h ago')).toBeInTheDocument(); + expect(screen.getByTitle('2026-05-24T13:00:00Z')).toBeInTheDocument(); expect(screen.getByText('0/3 nodes are available')).toBeInTheDocument(); expect( diff --git a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts index ddced6bb87..811c887294 100644 --- a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts +++ b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts @@ -13,6 +13,8 @@ import { compareKubernetesNodes, compareKubernetesPods, getKubernetesPageTabSpecs, + kubernetesClusterLabel, + kubernetesScopeLabel, mapKubernetesControllerStatus, mapKubernetesCronJobStatus, mapKubernetesDaemonSetStatus, @@ -807,6 +809,42 @@ describe('kubernetesPageModel', () => { }); }); + describe('kubernetesScopeLabel', () => { + it('prefers the human cluster name over the internal cluster id', () => { + expect( + kubernetesScopeLabel( + makeResource({ + id: 'pod-1', + type: 'pod', + kubernetes: { + clusterId: 'k8s-production-1', + clusterName: 'Production EU', + namespace: 'services', + }, + }), + ), + ).toBe('Production EU/services'); + expect( + kubernetesClusterLabel( + makeResource({ + id: 'pod-2', + type: 'pod', + kubernetes: { clusterId: 'k8s-production-1' }, + }), + ), + ).toBe('k8s-production-1'); + }); + + it('falls back through namespace-only and empty scopes', () => { + expect( + kubernetesScopeLabel( + makeResource({ id: 'ns-only', type: 'pod', kubernetes: { namespace: 'apps' } }), + ), + ).toBe('apps'); + expect(kubernetesScopeLabel(makeResource({ id: 'bare', type: 'pod' }))).toBe('Cluster'); + }); + }); + describe('mapKubernetesIncidentSeverity', () => { it('buckets severity strings into critical / warning / info', () => { expect(mapKubernetesIncidentSeverity('critical')).toBe('critical'); diff --git a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts index c76f27dbf2..a28e578b22 100644 --- a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts +++ b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts @@ -94,6 +94,21 @@ const displayName = (resource: Resource): string => asTrimmedString(resource.kubernetes?.podName) || resource.id; +// Scope labels prefer the human-facing cluster name; the internal cluster id +// is only a fallback for rows whose adapter carries no name. Every Kubernetes +// page table shares this so the same cluster never reads as "Production EU" +// in one column and "k8s-production-1" in another. +export const kubernetesClusterLabel = (resource: Resource): string => + asTrimmedString(resource.kubernetes?.clusterName) || + asTrimmedString(resource.kubernetes?.clusterId); + +export const kubernetesScopeLabel = (resource: Resource): string => { + const cluster = kubernetesClusterLabel(resource); + const namespace = asTrimmedString(resource.kubernetes?.namespace); + if (namespace) return cluster ? `${cluster}/${namespace}` : namespace; + return cluster || 'Cluster'; +}; + const containerHasFatalReason = (container: ResourceKubernetesPodContainerStatus): boolean => { const reason = normalizeKubernetesToken(container.reason); if (reason && POD_CONTAINER_FATAL_REASONS.has(reason)) return true;