From 3590393bed2a53044714b2fecd050ffb30660453 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 19 Mar 2026 02:24:00 +0000 Subject: [PATCH] Centralize resource graph presentation --- .../Infrastructure/ResourceChangeSummary.tsx | 14 ++-------- .../Infrastructure/ResourceGraphSummary.tsx | 26 +++++------------ .../resourceChangePresentation.test.ts | 26 +++++++++++++++++ .../resourceCorrelationPresentation.test.ts | 19 +++++++++++++ .../src/utils/resourceChangePresentation.ts | 14 ++++++++++ .../utils/resourceCorrelationPresentation.ts | 28 +++++++++++++++++++ 6 files changed, 96 insertions(+), 31 deletions(-) diff --git a/frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx b/frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx index 0c73dca05..33cea6fb7 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx @@ -5,6 +5,7 @@ import { buildInfrastructureResourceHref } from '@/routing/resourceLinks'; import { formatResourceChangeHeadline, formatResourceChangeKind, + sortResourceChangesByObservedAt, } from '@/utils/resourceChangePresentation'; interface ResourceChangeSummaryProps { @@ -18,19 +19,8 @@ interface ResourceChangeSummaryProps { class?: string; } -const sortRecentChanges = (changes: ResourceChange[]): ResourceChange[] => - [...changes].sort((left, right) => { - const observedDelta = - new Date(right.observedAt).getTime() - new Date(left.observedAt).getTime(); - if (observedDelta !== 0) { - return observedDelta; - } - - return right.id.localeCompare(left.id); - }); - export const ResourceChangeSummary: Component = (props) => { - const sortedChanges = createMemo(() => sortRecentChanges(props.changes ?? [])); + const sortedChanges = createMemo(() => sortResourceChangesByObservedAt(props.changes ?? [])); const maxChanges = () => props.maxChanges ?? sortedChanges().length; const visibleChanges = createMemo(() => sortedChanges().slice(0, maxChanges())); const hasChanges = () => visibleChanges().length > 0; diff --git a/frontend-modern/src/components/Infrastructure/ResourceGraphSummary.tsx b/frontend-modern/src/components/Infrastructure/ResourceGraphSummary.tsx index cfcfd0fad..6b437e7d7 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceGraphSummary.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceGraphSummary.tsx @@ -7,6 +7,7 @@ import { formatResourceCorrelationHeadline, formatResourceCorrelationPattern, formatResourceCorrelationSummary, + formatResourceGraphSummaryText, sortResourceCorrelations, } from '@/utils/resourceCorrelationPresentation'; @@ -22,12 +23,6 @@ interface ResourceGraphSummaryProps { maxCorrelations?: number; } -const formatPluralCount = (count: number, singular: string, plural: string): string => - `${count} ${count === 1 ? singular : plural}`; - -const formatSummaryParts = (parts: Array): string => - parts.filter((part): part is string => Boolean(part && part.trim())).join(' · '); - export const ResourceGraphSummary: Component = (props) => { const className = () => props.class?.trim() ?? ''; const correlations = createMemo(() => sortResourceCorrelations(props.correlations ?? [])); @@ -37,20 +32,13 @@ export const ResourceGraphSummary: Component = (props const maxCorrelations = () => props.maxCorrelations ?? 3; const hasContent = () => dependencies().length > 0 || dependents().length > 0 || correlations().length > 0; - const summaryText = () => - props.summaryText?.trim() || - formatSummaryParts([ - dependencies().length > 0 - ? formatPluralCount(dependencies().length, 'dependency', 'dependencies') - : null, - dependents().length > 0 - ? formatPluralCount(dependents().length, 'dependent', 'dependents') - : null, - correlations().length > 0 - ? formatPluralCount(correlations().length, 'correlation', 'correlations') - : null, - ]); + formatResourceGraphSummaryText({ + dependenciesCount: dependencies().length, + dependentsCount: dependents().length, + correlationsCount: correlations().length, + summaryText: props.summaryText, + }); return ( diff --git a/frontend-modern/src/utils/__tests__/resourceChangePresentation.test.ts b/frontend-modern/src/utils/__tests__/resourceChangePresentation.test.ts index 8a4e165d6..4337666f7 100644 --- a/frontend-modern/src/utils/__tests__/resourceChangePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/resourceChangePresentation.test.ts @@ -6,6 +6,7 @@ import { getResourceChangeKindPresentation, getResourceChangeSourceAdapterPresentation, getResourceChangeSourceTypePresentation, + sortResourceChangesByObservedAt, } from '@/utils/resourceChangePresentation'; describe('resourceChangePresentation utils', () => { @@ -52,4 +53,29 @@ describe('resourceChangePresentation utils', () => { plural: 'Proxmox adapters', }); }); + + it('sorts recent changes canonically', () => { + const sorted = sortResourceChangesByObservedAt([ + { + id: 'change-b', + resourceId: 'vm-42', + kind: 'config_update', + observedAt: '2026-03-18T12:00:00Z', + } as never, + { + id: 'change-a', + resourceId: 'vm-42', + kind: 'config_update', + observedAt: '2026-03-18T12:05:00Z', + } as never, + { + id: 'change-c', + resourceId: 'vm-42', + kind: 'config_update', + observedAt: '2026-03-18T12:05:00Z', + } as never, + ]); + + expect(sorted.map((change) => change.id)).toEqual(['change-c', 'change-a', 'change-b']); + }); }); diff --git a/frontend-modern/src/utils/__tests__/resourceCorrelationPresentation.test.ts b/frontend-modern/src/utils/__tests__/resourceCorrelationPresentation.test.ts index 58ada7a90..85bfbf7e3 100644 --- a/frontend-modern/src/utils/__tests__/resourceCorrelationPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/resourceCorrelationPresentation.test.ts @@ -5,6 +5,7 @@ import { formatResourceCorrelationHeadline, formatResourceCorrelationPattern, formatResourceCorrelationSummary, + formatResourceGraphSummaryText, sortResourceCorrelations, } from '@/utils/resourceCorrelationPresentation'; @@ -85,4 +86,22 @@ describe('resourceCorrelationPresentation utils', () => { expect(sorted.map((item) => item.source_id)).toEqual(['storage-3', 'storage-1', 'storage-2']); }); + + it('formats canonical graph summary text', () => { + expect( + formatResourceGraphSummaryText({ + dependenciesCount: 2, + dependentsCount: 1, + correlationsCount: 3, + }), + ).toBe('2 dependencies · 1 dependent · 3 correlations'); + expect( + formatResourceGraphSummaryText({ + dependenciesCount: 0, + dependentsCount: 0, + correlationsCount: 0, + summaryText: 'custom summary', + }), + ).toBe('custom summary'); + }); }); diff --git a/frontend-modern/src/utils/resourceChangePresentation.ts b/frontend-modern/src/utils/resourceChangePresentation.ts index e89039732..7ccf31e72 100644 --- a/frontend-modern/src/utils/resourceChangePresentation.ts +++ b/frontend-modern/src/utils/resourceChangePresentation.ts @@ -180,3 +180,17 @@ export function formatResourceChangeHeadline(change: ResourceChange): string { } return `${formatResourceChangeKind(change.kind)}: ${change.resourceId}`; } + +export function sortResourceChangesByObservedAt( + changes: readonly ResourceChange[], +): ResourceChange[] { + return [...changes].sort((left, right) => { + const observedDelta = + new Date(right.observedAt).getTime() - new Date(left.observedAt).getTime(); + if (observedDelta !== 0) { + return observedDelta; + } + + return right.id.localeCompare(left.id); + }); +} diff --git a/frontend-modern/src/utils/resourceCorrelationPresentation.ts b/frontend-modern/src/utils/resourceCorrelationPresentation.ts index 5854588ba..6b769aec5 100644 --- a/frontend-modern/src/utils/resourceCorrelationPresentation.ts +++ b/frontend-modern/src/utils/resourceCorrelationPresentation.ts @@ -94,3 +94,31 @@ export function sortResourceCorrelations( return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0); }); } + +const formatPluralCount = (count: number, singular: string, plural: string): string => + `${count} ${count === 1 ? singular : plural}`; + +const formatSummaryParts = (parts: Array): string => + parts.filter((part): part is string => Boolean(part && part.trim())).join(' · '); + +export function formatResourceGraphSummaryText(options: { + dependenciesCount: number; + dependentsCount: number; + correlationsCount: number; + summaryText?: string | null; +}): string { + return ( + options.summaryText?.trim() || + formatSummaryParts([ + options.dependenciesCount > 0 + ? formatPluralCount(options.dependenciesCount, 'dependency', 'dependencies') + : null, + options.dependentsCount > 0 + ? formatPluralCount(options.dependentsCount, 'dependent', 'dependents') + : null, + options.correlationsCount > 0 + ? formatPluralCount(options.correlationsCount, 'correlation', 'correlations') + : null, + ]) + ); +}