mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Fix Kubernetes event ordering
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user