diff --git a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx index f363e2173..2cef91979 100644 --- a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx +++ b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx @@ -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<{ {(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<{ diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx index 1badc4526..42feb49e7 100644 --- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx +++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx @@ -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(() => ( { 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']); }); }); diff --git a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts index 94399bba9..0d10efa7c 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 { 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']); + }); }); diff --git a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts index febf307bf..251c97970 100644 --- a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts +++ b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts @@ -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 = { 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,