Centralize resource graph presentation

This commit is contained in:
rcourtman
2026-03-19 02:24:00 +00:00
parent 6af50809a4
commit 3590393bed
6 changed files with 96 additions and 31 deletions
@@ -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<ResourceChangeSummaryProps> = (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;
@@ -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 | null | undefined>): string =>
parts.filter((part): part is string => Boolean(part && part.trim())).join(' · ');
export const ResourceGraphSummary: Component<ResourceGraphSummaryProps> = (props) => {
const className = () => props.class?.trim() ?? '';
const correlations = createMemo(() => sortResourceCorrelations(props.correlations ?? []));
@@ -37,20 +32,13 @@ export const ResourceGraphSummary: Component<ResourceGraphSummaryProps> = (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 (
<Show when={hasContent()}>
@@ -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']);
});
});
@@ -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');
});
});
@@ -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);
});
}
@@ -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 | null | undefined>): 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,
])
);
}