Fix Kubernetes event ordering

This commit is contained in:
rcourtman
2026-05-24 20:57:05 +01:00
parent 27e72ec8b4
commit 080c38a066
4 changed files with 123 additions and 8 deletions
@@ -1,4 +1,4 @@
import { For, Show, type Component, type JSX } from 'solid-js';
import { For, Show, createMemo, type Component, type JSX } from 'solid-js';
import { StatusDot } from '@/components/shared/StatusDot';
import { TableCard } from '@/components/shared/TableCard';
import { TableCardHeader } from '@/components/shared/TableCardHeader';
@@ -10,7 +10,6 @@ import {
TableHeader,
TableRow,
} from '@/components/shared/Table';
import { getSimpleStatusIndicator } from '@/utils/status';
import { asTrimmedString } from '@/utils/stringUtils';
import {
PLATFORM_HEALTH_FILTER_OPTIONS,
@@ -26,6 +25,7 @@ import {
type PlatformResourceStatusFilter,
} from '@/features/platformPage/sharedPlatformPage';
import type { Resource } from '@/types/resource';
import { compareKubernetesEvents, mapKubernetesEventSeverity } from './kubernetesPageModel';
const textValue = (value: string | undefined): string => asTrimmedString(value) || '—';
@@ -66,8 +66,9 @@ export const KubernetesEventsTable: Component<{
title?: string;
showToolbar?: boolean;
}> = (props) => {
const sortedEvents = createMemo(() => [...props.resources].sort(compareKubernetesEvents));
const tableState = createPlatformTableFilterState({
resources: () => props.resources,
resources: sortedEvents,
initialStatus: 'all' as PlatformResourceStatusFilter,
filter: filterPlatformResources,
});
@@ -148,7 +149,8 @@ export const KubernetesEventsTable: Component<{
<TableBody class={PLATFORM_TABLE_BODY_CLASS}>
<For each={tableState.filtered()}>
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const indicator = () =>
mapKubernetesEventSeverity(resource.kubernetes?.eventType);
const name = () => eventName(resource);
const scope = () => eventScope(resource);
const observed = () => observedTime(resource);
@@ -164,7 +166,7 @@ export const KubernetesEventsTable: Component<{
<StatusDot
size="sm"
variant={indicator().variant}
title={resource.status || 'unknown'}
title={indicator().label}
ariaHidden
/>
<span class="truncate font-semibold text-base-content" title={name()}>
@@ -27,13 +27,31 @@ afterEach(() => {
});
describe('KubernetesEventsTable', () => {
it('renders Kubernetes Event fields with object, count, time, and message context', () => {
it('renders Kubernetes Event fields with severity, newest-first ordering, and message context', () => {
render(() => (
<KubernetesEventsTable
resources={[
makeResource({
id: 'event-1',
id: 'normal-event',
type: 'k8s-event',
status: 'degraded',
kubernetes: {
clusterName: 'prod',
namespace: 'apps',
resourceKind: 'Event',
eventType: 'Normal',
reason: 'Scheduled',
involvedKind: 'Pod',
involvedName: 'checkout-web-456',
count: 1,
eventTime: '2026-05-24T12:00:00Z',
message: 'Successfully assigned apps/checkout-web-456 to prod-k8s-01',
},
}),
makeResource({
id: 'warning-event',
type: 'k8s-event',
status: 'online',
kubernetes: {
clusterName: 'prod',
namespace: 'apps',
@@ -60,11 +78,19 @@ describe('KubernetesEventsTable', () => {
expect(screen.getByText('Reason')).toBeInTheDocument();
expect(screen.getByText('Object')).toBeInTheDocument();
expect(screen.getByText('Observed')).toBeInTheDocument();
expect(screen.getByTitle('Warning')).toHaveClass('bg-amber-500');
expect(screen.getByTitle('Normal')).toHaveClass('bg-slate-400');
expect(screen.getByText('Warning')).toBeInTheDocument();
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();
expect(screen.getByText('0/3 nodes are available')).toBeInTheDocument();
expect(
Array.from(document.querySelectorAll('[data-kubernetes-event-row]')).map((row) =>
row.getAttribute('data-kubernetes-event-row'),
),
).toEqual(['warning-event', 'normal-event']);
});
});
@@ -3,6 +3,8 @@ import type { Resource } from '@/types/resource';
import {
KUBERNETES_TAB_SPECS,
buildKubernetesPageModel,
compareKubernetesEvents,
mapKubernetesEventSeverity,
resolveKubernetesPageTabId,
} from '../kubernetesPageModel';
@@ -132,4 +134,54 @@ describe('kubernetesPageModel', () => {
expect(model.nodes.map((r) => r.id)).toEqual(['merged-node-1']);
});
it('maps Kubernetes Event severity from the event type instead of generic resource status', () => {
expect(mapKubernetesEventSeverity('Warning')).toEqual({
variant: 'warning',
label: 'Warning',
});
expect(mapKubernetesEventSeverity('Normal')).toEqual({
variant: 'muted',
label: 'Normal',
});
expect(mapKubernetesEventSeverity(undefined)).toEqual({
variant: 'muted',
label: 'Unknown',
});
});
it('orders Kubernetes Events by observed time from newest to oldest', () => {
const older = makeResource({
id: 'older',
type: 'k8s-event',
kubernetes: {
eventTime: '2026-05-24T11:00:00Z',
},
});
const newerFromFirstSeen = makeResource({
id: 'newer-first-seen',
type: 'k8s-event',
kubernetes: {
firstSeen: '2026-05-24T13:00:00Z',
},
});
const newestFromCreatedAt = makeResource({
id: 'newest-created',
type: 'k8s-event',
kubernetes: {
createdAt: '2026-05-24T14:00:00Z',
},
});
expect([older, newestFromCreatedAt, newerFromFirstSeen].sort(compareKubernetesEvents)).toEqual([
newestFromCreatedAt,
newerFromFirstSeen,
older,
]);
expect(
buildKubernetesPageModel([older, newestFromCreatedAt, newerFromFirstSeen]).events.map(
(event) => event.id,
),
).toEqual(['newest-created', 'newer-first-seen', 'older']);
});
});
@@ -1,4 +1,5 @@
import type { Resource, ResourceType } from '@/types/resource';
import type { StatusIndicator } from '@/utils/status';
import { resolveResourcePlatformType } from '@/utils/sourcePlatforms';
export type KubernetesPageTabId =
@@ -32,6 +33,38 @@ export const KUBERNETES_TAB_SPECS: readonly KubernetesTabSpec[] = [
const asTrimmedString = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
const eventTypeLabel = (value: string, fallback: string): string => {
const trimmed = asTrimmedString(value);
if (!trimmed) return fallback;
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
};
export function mapKubernetesEventSeverity(eventType: string | undefined): StatusIndicator {
const normalized = asTrimmedString(eventType).toLowerCase();
if (normalized === 'warning') return { variant: 'warning', label: 'Warning' };
if (normalized === 'normal') return { variant: 'muted', label: 'Normal' };
return {
variant: 'muted',
label: eventTypeLabel(eventType ?? '', 'Unknown'),
};
}
const parseKubernetesEventObservedTime = (resource: Resource): number => {
const observed =
asTrimmedString(resource.kubernetes?.eventTime) ||
asTrimmedString(resource.kubernetes?.firstSeen) ||
asTrimmedString(resource.kubernetes?.createdAt);
if (!observed) return 0;
const timestamp = Date.parse(observed);
return Number.isFinite(timestamp) ? timestamp : 0;
};
export const compareKubernetesEvents = (left: Resource, right: Resource): number => {
const timeDelta = parseKubernetesEventObservedTime(right) - parseKubernetesEventObservedTime(left);
if (timeDelta !== 0) return timeDelta;
return left.id.localeCompare(right.id);
};
const KUBERNETES_ROUTE_TAB_ALIASES: Record<string, KubernetesPageTabId> = {
autoscaling: 'workloads',
config: 'configuration',
@@ -162,7 +195,9 @@ export function buildKubernetesPageModel(resources: Resource[]): KubernetesPageM
const horizontalPodAutoscalers = k8sResources.filter(
(resource) => resource.type === 'k8s-horizontal-pod-autoscaler',
);
const events = k8sResources.filter((resource) => resource.type === 'k8s-event');
const events = k8sResources
.filter((resource) => resource.type === 'k8s-event')
.sort(compareKubernetesEvents);
const workloads = [
...deployments,
...replicaSets,