mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-22 19:56:01 +00:00
kubernetes(tables): shared scope labels, humanized event times, drop Observed
Noise findings from the v5-parity audit: - Eight tables carried copy-pasted scope helpers that preferred the internal cluster id over the display name, so the pods table read "k8s-production-1/services" while the deployments table said "Production EU" for the same cluster. Replace every local helper with shared kubernetesScopeLabel / kubernetesClusterLabel (name first, id fallback) in the page model. - The events Observed column rendered the raw ISO timestamp with microseconds; humanize it like every other age on the page and keep the raw timestamp on the hover title. - Drop the Deployments Observed column: raw observedGeneration with no spec generation beside it fails the what-does-the-user-do test, the same shape as the Source badge removed from the disks table in #1471. The value still ships in the resource payload.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 => (
|
||||
<span class="tabular-nums">{value ?? 0}</span>
|
||||
@@ -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.
|
||||
*/}
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[22%]`}>
|
||||
<TableHead class={`${getPlatformTableHeadClassForKind('name')} md:w-[25%]`}>
|
||||
Deployment
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[17%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[20%]`}
|
||||
>
|
||||
Namespace
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[14%]`}
|
||||
class={`${getPlatformTableHeadClassForKind('text')} hidden md:table-cell md:w-[17%]`}
|
||||
>
|
||||
Cluster
|
||||
</TableHead>
|
||||
@@ -157,11 +159,6 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
>
|
||||
Available
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} hidden md:table-cell md:w-[9%]`}
|
||||
>
|
||||
Observed
|
||||
</TableHead>
|
||||
<TableHead
|
||||
class={`${getPlatformTableHeadClassForKind('numeric-value')} md:w-[6%]`}
|
||||
>
|
||||
@@ -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)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
{replicaCount(deployment.kubernetes?.observedGeneration)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
>
|
||||
@@ -253,7 +242,7 @@ export const KubernetesDeploymentsTable: Component<{
|
||||
resource={deployment}
|
||||
open={isExpanded()}
|
||||
detailRowId={detailRowId()}
|
||||
colSpan={9}
|
||||
colSpan={8}
|
||||
resolveResourceLabel={resolveResourceLabel}
|
||||
onClose={() => drawer.close(deployment)}
|
||||
/>
|
||||
|
||||
@@ -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' ? <span class="tabular-nums">{value}</span> : <span>—</span>;
|
||||
@@ -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<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content md:table-cell`}
|
||||
>
|
||||
<span class="inline-block max-w-[12rem] truncate" title={observed()}>
|
||||
<span
|
||||
class="inline-block max-w-[12rem] truncate"
|
||||
title={observedTimestamp(resource) || observed()}
|
||||
>
|
||||
{observed()}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+4
-2
@@ -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"]'),
|
||||
|
||||
@@ -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(() => (
|
||||
<KubernetesEventsTable
|
||||
resources={[
|
||||
@@ -84,7 +88,10 @@ describe('KubernetesEventsTable', () => {
|
||||
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(
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user