mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 08:57:12 +00:00
Share canonical resource change labels
This commit is contained in:
parent
af05d195b9
commit
d00bb48bb2
6 changed files with 199 additions and 139 deletions
|
|
@ -88,13 +88,16 @@ the unified table as well, so PBS and PMG entries must keep the same bounded
|
|||
presentation and verification surface as the primary fleet rows. The shared
|
||||
`ResourceFacetSummary` component now owns that chip rendering path, so any
|
||||
future summary changes must preserve the same bounded row budget instead of
|
||||
forking separate table-only presentation logic. Row summaries now also prefer
|
||||
canonical `facetCounts` on each resource when they are available, so the hot
|
||||
path can stay within the same budget while still reading totals from the
|
||||
shared resource contract. The drawer history surface reuses the same governed
|
||||
resource route helpers for relationship and related-resource links, so cross-
|
||||
resource navigation stays within the existing infrastructure surface rather
|
||||
than branching into custom detail-only routing.
|
||||
forking separate table-only presentation logic. That component now also
|
||||
consumes the shared `frontend-modern/src/utils/resourceChangePresentation.ts`
|
||||
label helper for canonical change kinds, source types, and adapter provenance
|
||||
so the chip wording stays consistent without adding extra hot-path branching.
|
||||
Row summaries now also prefer canonical `facetCounts` on each resource when
|
||||
they are available, so the hot path can stay within the same budget while
|
||||
still reading totals from the shared resource contract. The drawer history
|
||||
surface reuses the same governed resource route helpers for relationship and
|
||||
related-resource links, so cross-resource navigation stays within the existing
|
||||
infrastructure surface rather than branching into custom detail-only routing.
|
||||
Governance metadata such as sensitivity and routing scope may be visible in
|
||||
the table, but it must remain on the same bounded row-windowing and mounted-row
|
||||
budget proved by `UnifiedResourceTable.performance.contract.test.tsx` rather
|
||||
|
|
|
|||
|
|
@ -229,6 +229,10 @@ timeline endpoint is paginated. Relationship and timeline references in that
|
|||
drawer now route through the canonical infrastructure resource filter, so the
|
||||
resource graph remains navigable from the history surface instead of being
|
||||
purely descriptive text.
|
||||
`ResourceFacetSummary` now consumes the shared
|
||||
`frontend-modern/src/utils/resourceChangePresentation.ts` label helper for
|
||||
canonical change kinds, source types, and adapter provenance, so the chip
|
||||
wording stays aligned across table, drawer, and intelligence surfaces.
|
||||
Relationship cards in that drawer also surface `lastSeenAt` freshness and
|
||||
optional metadata blocks, and timeline cards surface change metadata when it
|
||||
is present, so the graph history view preserves the richer provenance already
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@ import { For, Show, createMemo, type Component } from 'solid-js';
|
|||
import type {
|
||||
ResourceCapability,
|
||||
ResourceChange,
|
||||
ResourceChangeKind,
|
||||
ResourceFacetCounts,
|
||||
ResourceFacetSourceAdapter,
|
||||
ResourceRelationship,
|
||||
} from '@/types/resource';
|
||||
import {
|
||||
RESOURCE_CHANGE_KIND_ORDER,
|
||||
RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER,
|
||||
RESOURCE_CHANGE_SOURCE_TYPE_ORDER,
|
||||
getResourceChangeKindPresentation,
|
||||
getResourceChangeSourceAdapterPresentation,
|
||||
getResourceChangeSourceTypePresentation,
|
||||
} from '@/utils/resourceChangePresentation';
|
||||
|
||||
type FacetBadge = {
|
||||
label: string;
|
||||
|
|
@ -29,130 +35,6 @@ const badgeBase =
|
|||
const countLabel = (count: number, singular: string, plural = `${singular}s`) =>
|
||||
`${count} ${count === 1 ? singular : plural}`;
|
||||
|
||||
const recentChangeKindOrder: ResourceChangeKind[] = [
|
||||
'state_transition',
|
||||
'restart',
|
||||
'config_update',
|
||||
'metric_anomaly',
|
||||
'relationship_change',
|
||||
'capability_change',
|
||||
];
|
||||
|
||||
const recentChangeKindLabels: Record<
|
||||
ResourceChangeKind,
|
||||
{ label: string; plural: string; className: string }
|
||||
> = {
|
||||
state_transition: {
|
||||
label: 'State transition',
|
||||
plural: 'State transitions',
|
||||
className: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
||||
},
|
||||
restart: {
|
||||
label: 'Restart',
|
||||
plural: 'Restarts',
|
||||
className: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300',
|
||||
},
|
||||
config_update: {
|
||||
label: 'Config update',
|
||||
plural: 'Config updates',
|
||||
className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
},
|
||||
metric_anomaly: {
|
||||
label: 'Anomaly',
|
||||
plural: 'Anomalies',
|
||||
className: 'bg-rose-100 text-rose-700 dark:bg-rose-900 dark:text-rose-300',
|
||||
},
|
||||
relationship_change: {
|
||||
label: 'Relationship change',
|
||||
plural: 'Relationship changes',
|
||||
className: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300',
|
||||
},
|
||||
capability_change: {
|
||||
label: 'Capability change',
|
||||
plural: 'Capability changes',
|
||||
className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900 dark:text-cyan-300',
|
||||
},
|
||||
};
|
||||
|
||||
type RecentChangeSourceType =
|
||||
| 'platform_event'
|
||||
| 'pulse_diff'
|
||||
| 'heuristic'
|
||||
| 'user_action'
|
||||
| 'agent_action';
|
||||
|
||||
const recentChangeSourceTypeOrder: RecentChangeSourceType[] = [
|
||||
'platform_event',
|
||||
'pulse_diff',
|
||||
'heuristic',
|
||||
'user_action',
|
||||
'agent_action',
|
||||
];
|
||||
|
||||
const recentChangeSourceTypeLabels: Record<
|
||||
RecentChangeSourceType,
|
||||
{ label: string; plural: string; className: string }
|
||||
> = {
|
||||
platform_event: {
|
||||
label: 'Platform event',
|
||||
plural: 'Platform events',
|
||||
className: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
|
||||
},
|
||||
pulse_diff: {
|
||||
label: 'Pulse diff',
|
||||
plural: 'Pulse diffs',
|
||||
className: 'bg-violet-100 text-violet-700 dark:bg-violet-900 dark:text-violet-300',
|
||||
},
|
||||
heuristic: {
|
||||
label: 'Heuristic',
|
||||
plural: 'Heuristics',
|
||||
className: 'bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-900 dark:text-fuchsia-300',
|
||||
},
|
||||
user_action: {
|
||||
label: 'User action',
|
||||
plural: 'User actions',
|
||||
className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
},
|
||||
agent_action: {
|
||||
label: 'Agent action',
|
||||
plural: 'Agent actions',
|
||||
className: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
||||
},
|
||||
};
|
||||
|
||||
const recentChangeSourceAdapterOrder: ResourceFacetSourceAdapter[] = [
|
||||
'docker_adapter',
|
||||
'proxmox_adapter',
|
||||
'truenas_adapter',
|
||||
'agent:ops-helper',
|
||||
];
|
||||
|
||||
const recentChangeSourceAdapterLabels: Record<
|
||||
ResourceFacetSourceAdapter,
|
||||
{ label: string; plural: string; className: string }
|
||||
> = {
|
||||
docker_adapter: {
|
||||
label: 'Docker adapter',
|
||||
plural: 'Docker adapters',
|
||||
className: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300',
|
||||
},
|
||||
proxmox_adapter: {
|
||||
label: 'Proxmox adapter',
|
||||
plural: 'Proxmox adapters',
|
||||
className: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
|
||||
},
|
||||
truenas_adapter: {
|
||||
label: 'TrueNAS adapter',
|
||||
plural: 'TrueNAS adapters',
|
||||
className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
},
|
||||
'agent:ops-helper': {
|
||||
label: 'Ops helper',
|
||||
plural: 'Ops helpers',
|
||||
className: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
||||
},
|
||||
};
|
||||
|
||||
const buildFacetBadges = (
|
||||
capabilities?: readonly ResourceCapability[] | null,
|
||||
relationships?: readonly ResourceRelationship[] | null,
|
||||
|
|
@ -190,10 +72,10 @@ const buildFacetBadges = (
|
|||
|
||||
const kindCounts = counts?.recentChangeKinds;
|
||||
if (kindCounts) {
|
||||
for (const kind of recentChangeKindOrder) {
|
||||
for (const kind of RESOURCE_CHANGE_KIND_ORDER) {
|
||||
const count = kindCounts[kind];
|
||||
if (!count || count <= 0) continue;
|
||||
const kindLabel = recentChangeKindLabels[kind];
|
||||
const kindLabel = getResourceChangeKindPresentation(kind);
|
||||
badges.push({
|
||||
label: `${kindLabel.label} ${count}`,
|
||||
title: countLabel(count, kindLabel.label.toLowerCase(), kindLabel.plural.toLowerCase()),
|
||||
|
|
@ -204,10 +86,10 @@ const buildFacetBadges = (
|
|||
|
||||
const sourceTypeCounts = counts?.recentChangeSourceTypes;
|
||||
if (sourceTypeCounts) {
|
||||
for (const sourceType of recentChangeSourceTypeOrder) {
|
||||
for (const sourceType of RESOURCE_CHANGE_SOURCE_TYPE_ORDER) {
|
||||
const count = sourceTypeCounts[sourceType];
|
||||
if (!count || count <= 0) continue;
|
||||
const sourceTypeLabel = recentChangeSourceTypeLabels[sourceType];
|
||||
const sourceTypeLabel = getResourceChangeSourceTypePresentation(sourceType);
|
||||
badges.push({
|
||||
label: `${sourceTypeLabel.label} ${count}`,
|
||||
title: countLabel(
|
||||
|
|
@ -222,10 +104,10 @@ const buildFacetBadges = (
|
|||
|
||||
const sourceAdapterCounts = counts?.recentChangeSourceAdapters;
|
||||
if (sourceAdapterCounts) {
|
||||
for (const sourceAdapter of recentChangeSourceAdapterOrder) {
|
||||
for (const sourceAdapter of RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER) {
|
||||
const count = sourceAdapterCounts[sourceAdapter];
|
||||
if (!count || count <= 0) continue;
|
||||
const sourceAdapterLabel = recentChangeSourceAdapterLabels[sourceAdapter];
|
||||
const sourceAdapterLabel = getResourceChangeSourceAdapterPresentation(sourceAdapter);
|
||||
badges.push({
|
||||
label: `${sourceAdapterLabel.label} ${count}`,
|
||||
title: countLabel(
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
recentChanges: 3,
|
||||
recentChangeKinds: {
|
||||
restart: 2,
|
||||
config_update: 1,
|
||||
metric_anomaly: 1,
|
||||
},
|
||||
recentChangeSourceTypes: {
|
||||
|
|
@ -173,6 +174,7 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(getByText('Relationships 1')).toBeInTheDocument();
|
||||
expect(getByText('Timeline 3')).toBeInTheDocument();
|
||||
expect(getByText('Restart 2')).toBeInTheDocument();
|
||||
expect(getByText('Config update 1')).toBeInTheDocument();
|
||||
expect(getByText('Anomaly 1')).toBeInTheDocument();
|
||||
expect(getByText('Platform event 1')).toBeInTheDocument();
|
||||
expect(getByText('Pulse diff 2')).toBeInTheDocument();
|
||||
|
|
@ -210,6 +212,7 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
recentChanges: 3,
|
||||
recentChangeKinds: {
|
||||
restart: 2,
|
||||
config_update: 1,
|
||||
metric_anomaly: 1,
|
||||
},
|
||||
recentChangeSourceTypes: {
|
||||
|
|
@ -241,6 +244,7 @@ describe('UnifiedResourceTable performance contract', () => {
|
|||
expect(getByText('Capabilities 1')).toBeInTheDocument();
|
||||
expect(getByText('Relationships 1')).toBeInTheDocument();
|
||||
expect(getByText('Timeline 3')).toBeInTheDocument();
|
||||
expect(getByText('Config update 1')).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(getBodyRowCount(container)).toBe(PROFILES.S);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { describe, expect, it } from 'vitest';
|
|||
import {
|
||||
formatResourceChangeHeadline,
|
||||
formatResourceChangeKind,
|
||||
getResourceChangeKindPresentation,
|
||||
getResourceChangeSourceAdapterPresentation,
|
||||
getResourceChangeSourceTypePresentation,
|
||||
} from '@/utils/resourceChangePresentation';
|
||||
|
||||
describe('resourceChangePresentation utils', () => {
|
||||
|
|
@ -34,4 +37,19 @@ describe('resourceChangePresentation utils', () => {
|
|||
} as never),
|
||||
).toBe('Config update: Updated canonical config');
|
||||
});
|
||||
|
||||
it('exposes canonical kind, source type, and adapter presentations', () => {
|
||||
expect(getResourceChangeKindPresentation('restart')).toMatchObject({
|
||||
label: 'Restart',
|
||||
plural: 'Restarts',
|
||||
});
|
||||
expect(getResourceChangeSourceTypePresentation('platform_event')).toMatchObject({
|
||||
label: 'Platform event',
|
||||
plural: 'Platform events',
|
||||
});
|
||||
expect(getResourceChangeSourceAdapterPresentation('proxmox_adapter')).toMatchObject({
|
||||
label: 'Proxmox adapter',
|
||||
plural: 'Proxmox adapters',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,153 @@
|
|||
import type { ResourceChange } from '@/types/resource';
|
||||
import type { ResourceChangeKind, ResourceFacetSourceAdapter } from '@/types/resource';
|
||||
|
||||
export interface ResourceChangeLabelPresentation {
|
||||
label: string;
|
||||
plural: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const RESOURCE_CHANGE_KIND_ORDER: ResourceChangeKind[] = [
|
||||
'state_transition',
|
||||
'restart',
|
||||
'config_update',
|
||||
'metric_anomaly',
|
||||
'relationship_change',
|
||||
'capability_change',
|
||||
];
|
||||
|
||||
const RESOURCE_CHANGE_KIND_PRESENTATIONS: Record<
|
||||
ResourceChangeKind,
|
||||
ResourceChangeLabelPresentation
|
||||
> = {
|
||||
state_transition: {
|
||||
label: 'State transition',
|
||||
plural: 'State transitions',
|
||||
className: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
||||
},
|
||||
restart: {
|
||||
label: 'Restart',
|
||||
plural: 'Restarts',
|
||||
className: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300',
|
||||
},
|
||||
config_update: {
|
||||
label: 'Config update',
|
||||
plural: 'Config updates',
|
||||
className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
},
|
||||
metric_anomaly: {
|
||||
label: 'Anomaly',
|
||||
plural: 'Anomalies',
|
||||
className: 'bg-rose-100 text-rose-700 dark:bg-rose-900 dark:text-rose-300',
|
||||
},
|
||||
relationship_change: {
|
||||
label: 'Relationship change',
|
||||
plural: 'Relationship changes',
|
||||
className: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900 dark:text-indigo-300',
|
||||
},
|
||||
capability_change: {
|
||||
label: 'Capability change',
|
||||
plural: 'Capability changes',
|
||||
className: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900 dark:text-cyan-300',
|
||||
},
|
||||
};
|
||||
|
||||
type ResourceChangeSourceType =
|
||||
| 'platform_event'
|
||||
| 'pulse_diff'
|
||||
| 'heuristic'
|
||||
| 'user_action'
|
||||
| 'agent_action';
|
||||
|
||||
export const RESOURCE_CHANGE_SOURCE_TYPE_ORDER: ResourceChangeSourceType[] = [
|
||||
'platform_event',
|
||||
'pulse_diff',
|
||||
'heuristic',
|
||||
'user_action',
|
||||
'agent_action',
|
||||
];
|
||||
|
||||
const RESOURCE_CHANGE_SOURCE_TYPE_PRESENTATIONS: Record<
|
||||
ResourceChangeSourceType,
|
||||
ResourceChangeLabelPresentation
|
||||
> = {
|
||||
platform_event: {
|
||||
label: 'Platform event',
|
||||
plural: 'Platform events',
|
||||
className: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
|
||||
},
|
||||
pulse_diff: {
|
||||
label: 'Pulse diff',
|
||||
plural: 'Pulse diffs',
|
||||
className: 'bg-violet-100 text-violet-700 dark:bg-violet-900 dark:text-violet-300',
|
||||
},
|
||||
heuristic: {
|
||||
label: 'Heuristic',
|
||||
plural: 'Heuristics',
|
||||
className: 'bg-fuchsia-100 text-fuchsia-700 dark:bg-fuchsia-900 dark:text-fuchsia-300',
|
||||
},
|
||||
user_action: {
|
||||
label: 'User action',
|
||||
plural: 'User actions',
|
||||
className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
},
|
||||
agent_action: {
|
||||
label: 'Agent action',
|
||||
plural: 'Agent actions',
|
||||
className: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
||||
},
|
||||
};
|
||||
|
||||
export const RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER: ResourceFacetSourceAdapter[] = [
|
||||
'docker_adapter',
|
||||
'proxmox_adapter',
|
||||
'truenas_adapter',
|
||||
'agent:ops-helper',
|
||||
];
|
||||
|
||||
const RESOURCE_CHANGE_SOURCE_ADAPTER_PRESENTATIONS: Record<
|
||||
ResourceFacetSourceAdapter,
|
||||
ResourceChangeLabelPresentation
|
||||
> = {
|
||||
docker_adapter: {
|
||||
label: 'Docker adapter',
|
||||
plural: 'Docker adapters',
|
||||
className: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300',
|
||||
},
|
||||
proxmox_adapter: {
|
||||
label: 'Proxmox adapter',
|
||||
plural: 'Proxmox adapters',
|
||||
className: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
|
||||
},
|
||||
truenas_adapter: {
|
||||
label: 'TrueNAS adapter',
|
||||
plural: 'TrueNAS adapters',
|
||||
className: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
||||
},
|
||||
'agent:ops-helper': {
|
||||
label: 'Ops helper',
|
||||
plural: 'Ops helpers',
|
||||
className: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
||||
},
|
||||
};
|
||||
|
||||
export function getResourceChangeKindPresentation(
|
||||
kind: ResourceChangeKind,
|
||||
): ResourceChangeLabelPresentation {
|
||||
return RESOURCE_CHANGE_KIND_PRESENTATIONS[kind];
|
||||
}
|
||||
|
||||
export function getResourceChangeSourceTypePresentation(
|
||||
sourceType: ResourceChangeSourceType,
|
||||
): ResourceChangeLabelPresentation {
|
||||
return RESOURCE_CHANGE_SOURCE_TYPE_PRESENTATIONS[sourceType];
|
||||
}
|
||||
|
||||
export function getResourceChangeSourceAdapterPresentation(
|
||||
sourceAdapter: ResourceFacetSourceAdapter,
|
||||
): ResourceChangeLabelPresentation {
|
||||
return RESOURCE_CHANGE_SOURCE_ADAPTER_PRESENTATIONS[sourceAdapter];
|
||||
}
|
||||
|
||||
export function formatResourceChangeKind(kind: ResourceChange['kind']): string {
|
||||
switch (kind) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue