From 5d75d562f6409836fe5780d34b9b6e7ac57117eb Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Apr 2026 22:49:28 +0100 Subject: [PATCH] Collapse OCI as a workload kind and unify group-header labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCI is a flavor of system-container, not a distinct workload kind, but WorkloadTypePresentationKey had 'oci-container' with its own purple badge — creating two presentation paths for the same thing. Group headers in workloadSelectors had inline label strings ('App Containers', 'Container', 'Pods', 'VM') that drifted from the row badge's PRESENTATION_MAP whenever either side changed. Drop 'oci-container' from the workload-kind union and presentation map; normalize 'oci-container' input to 'system-container'. Add pluralLabel to WorkloadTypePresentation and route getWorkloadGroupLabel through it so the row badge and group header share one source. Collapse the matching branch in recoveryItemTypePresentation. Align the relevant assertion tests with the canonical VM/LXC/Container/Pod labels. --- .../components/Dashboard/workloadSelectors.ts | 13 ++++++--- .../__tests__/workloadTypeBadges.test.ts | 16 +++++----- .../recoveryItemTypePresentation.test.ts | 2 +- .../recoveryTablePresentation.test.ts | 4 +-- .../workloadTypePresentation.test.ts | 4 +-- .../src/utils/recoveryItemTypePresentation.ts | 11 +------ .../src/utils/workloadTypePresentation.ts | 29 +++++++++++-------- 7 files changed, 40 insertions(+), 39 deletions(-) diff --git a/frontend-modern/src/components/Dashboard/workloadSelectors.ts b/frontend-modern/src/components/Dashboard/workloadSelectors.ts index 87fded87c2..975606ea52 100644 --- a/frontend-modern/src/components/Dashboard/workloadSelectors.ts +++ b/frontend-modern/src/components/Dashboard/workloadSelectors.ts @@ -8,6 +8,7 @@ import { normalizeSourcePlatformQueryValue } from '@/utils/sourcePlatforms'; import { DEGRADED_HEALTH_STATUSES, OFFLINE_HEALTH_STATUSES } from '@/utils/status'; import { getNodeDisplayName } from '@/utils/nodes'; import { getCanonicalWorkloadId, resolveWorkloadType } from '@/utils/workloads'; +import { getWorkloadTypePresentation } from '@/utils/workloadTypePresentation'; import { buildNodeByInstance, getKubernetesContextKey, workloadNodeScopeId } from './workloadTopology'; export interface FilterWorkloadsParams { @@ -263,10 +264,14 @@ export const getWorkloadGroupLabel = ( if (normalizedNodeName) { return { type: '', name: normalizedNodeName }; } - if (prefix === 'app-container') return { type: 'App Containers', name: context }; - if (prefix === 'pod') return { type: 'Pods', name: context }; - if (prefix === 'vm') return { type: 'VM', name: context }; - if (prefix === 'system-container') return { type: 'Container', name: context }; + if ( + prefix === 'app-container' || + prefix === 'pod' || + prefix === 'vm' || + prefix === 'system-container' + ) { + return { type: getWorkloadTypePresentation(prefix).pluralLabel, name: context }; + } const first = guests[0]; if (first) { const cluster = (first.clusterName || '').trim(); diff --git a/frontend-modern/src/components/shared/__tests__/workloadTypeBadges.test.ts b/frontend-modern/src/components/shared/__tests__/workloadTypeBadges.test.ts index 2ea84d2d11..f5547c48fb 100644 --- a/frontend-modern/src/components/shared/__tests__/workloadTypeBadges.test.ts +++ b/frontend-modern/src/components/shared/__tests__/workloadTypeBadges.test.ts @@ -30,9 +30,9 @@ describe('workloadTypeBadges', () => { }); describe('System container types', () => { - it('returns Container badge for system-container', () => { + it('returns LXC badge for system-container', () => { const result = getWorkloadTypeBadge('system-container'); - expect(result.label).toBe('Container'); + expect(result.label).toBe('LXC'); expect(result.title).toBe('System Container'); }); @@ -50,14 +50,14 @@ describe('workloadTypeBadges', () => { }); describe('App container types', () => { - it('returns Containers badge for canonical app-container', () => { + it('returns Container badge for canonical app-container', () => { const result = getWorkloadTypeBadge('app-container'); - expect(result.label).toBe('Containers'); + expect(result.label).toBe('Container'); }); it('retains docker alias compatibility', () => { const result = getWorkloadTypeBadge('docker'); - expect(result.label).toBe('Containers'); + expect(result.label).toBe('Container'); }); it('does not normalize removed docker-container alias', () => { @@ -105,10 +105,10 @@ describe('workloadTypeBadges', () => { }); describe('OCI types', () => { - it('returns OCI badge for oci-container', () => { + it('collapses oci-container into the LXC badge — OCI is a flavor of system-container, not a distinct kind', () => { const result = getWorkloadTypeBadge('oci-container'); - expect(result.label).toBe('OCI'); - expect(result.title).toBe('OCI Container'); + expect(result.label).toBe('LXC'); + expect(result.title).toBe('System Container'); }); }); diff --git a/frontend-modern/src/utils/__tests__/recoveryItemTypePresentation.test.ts b/frontend-modern/src/utils/__tests__/recoveryItemTypePresentation.test.ts index 58be2b58ab..e328557e9a 100644 --- a/frontend-modern/src/utils/__tests__/recoveryItemTypePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/recoveryItemTypePresentation.test.ts @@ -27,7 +27,7 @@ describe('recoveryItemTypePresentation', () => { }); expect(getRecoveryItemTypePresentation('system-container')).toMatchObject({ key: 'system-container', - label: 'Container', + label: 'LXC', }); expect(getRecoveryItemTypePresentation('app-container')).toMatchObject({ key: 'app-container', diff --git a/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts b/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts index 13a7d3c0b0..183bd29b28 100644 --- a/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/recoveryTablePresentation.test.ts @@ -84,13 +84,13 @@ describe('recoveryTablePresentation', () => { expect(getRecoveryPointItemTypeBadgeClass(point)).toContain('px-1 py-0.5'); }); - it('normalizes proxmox lxc subjects to the canonical container badge', () => { + it('normalizes proxmox lxc subjects to the canonical LXC badge', () => { const point = { display: { subjectType: 'proxmox-lxc' }, subjectRef: { type: 'proxmox-lxc' }, } as RecoveryPoint; - expect(getRecoveryPointItemTypeLabel(point)).toBe('Container'); + expect(getRecoveryPointItemTypeLabel(point)).toBe('LXC'); expect(getRecoveryPointItemTypeBadgeClass(point)).toContain('bg-green-100'); expect(getRecoveryPointItemTypeBadgeClass(point)).toContain('text-green-700'); expect(getRecoveryPointItemTypeBadgeClass(point)).toContain('px-1 py-0.5'); diff --git a/frontend-modern/src/utils/__tests__/workloadTypePresentation.test.ts b/frontend-modern/src/utils/__tests__/workloadTypePresentation.test.ts index 7abf998981..23720d2e98 100644 --- a/frontend-modern/src/utils/__tests__/workloadTypePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/workloadTypePresentation.test.ts @@ -34,7 +34,7 @@ describe('workloadTypePresentation', () => { }); it('retains canonical alias compatibility through the utility layer', () => { - expect(getWorkloadTypePresentation('docker').label).toBe('Containers'); + expect(getWorkloadTypePresentation('docker').label).toBe('Container'); expect(getWorkloadTypePresentation('host').label).toBe('Agent'); expect(getWorkloadTypePresentation('k8s').label).toBe('Pod'); }); @@ -51,7 +51,7 @@ describe('workloadTypePresentation', () => { describe('getWorkloadTypeLabel', () => { it('returns the presentation label for known and unknown workload types', () => { expect(getWorkloadTypeLabel('vm')).toBe('VM'); - expect(getWorkloadTypeLabel('docker')).toBe('Containers'); + expect(getWorkloadTypeLabel('docker')).toBe('Container'); expect(getWorkloadTypeLabel('custom-type')).toBe('Custom Type'); }); }); diff --git a/frontend-modern/src/utils/recoveryItemTypePresentation.ts b/frontend-modern/src/utils/recoveryItemTypePresentation.ts index ab708521d2..eb36944433 100644 --- a/frontend-modern/src/utils/recoveryItemTypePresentation.ts +++ b/frontend-modern/src/utils/recoveryItemTypePresentation.ts @@ -62,7 +62,7 @@ export const normalizeRecoveryItemTypeQueryValue = ( case 'app-container': return 'app-container'; case 'oci-container': - return 'oci-container'; + return 'system-container'; case 'k8s-pod': case 'pod': return 'pod'; @@ -122,15 +122,6 @@ export const getRecoveryItemTypePresentation = ( tableBadgeClasses: `${TABLE_BADGE_BASE_CLASSES} ${presentation.className}`, }; } - case 'oci-container': { - const presentation = getWorkloadTypePresentation('oci-container'); - return { - key, - label: presentation.label, - badgeClasses: `${BADGE_BASE_CLASSES} ${presentation.className}`, - tableBadgeClasses: `${TABLE_BADGE_BASE_CLASSES} ${presentation.className}`, - }; - } case 'pod': { const presentation = getWorkloadTypePresentation('pod'); return { diff --git a/frontend-modern/src/utils/workloadTypePresentation.ts b/frontend-modern/src/utils/workloadTypePresentation.ts index 7e21dfe4b5..958159dff6 100644 --- a/frontend-modern/src/utils/workloadTypePresentation.ts +++ b/frontend-modern/src/utils/workloadTypePresentation.ts @@ -4,6 +4,7 @@ import { titleCaseDelimitedLabel } from '@/utils/textPresentation'; export interface WorkloadTypePresentation { label: string; + pluralLabel: string; title: string; className: string; } @@ -13,44 +14,44 @@ type WorkloadTypePresentationKey = | 'system-container' | 'app-container' | 'agent' - | 'pod' - | 'oci-container'; + | 'pod'; const PRESENTATION_MAP: Record = { vm: { label: 'VM', + pluralLabel: 'VMs', title: 'Virtual Machine', className: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300', }, 'system-container': { - label: 'Container', + label: 'LXC', + pluralLabel: 'LXC', title: 'System Container', className: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300', }, 'app-container': { - label: 'Containers', + label: 'Container', + pluralLabel: 'Containers', title: 'Application Container', className: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300', }, pod: { label: 'Pod', + pluralLabel: 'Pods', title: 'Kubernetes Pod', className: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300', }, agent: { label: 'Agent', + pluralLabel: 'Agents', title: 'Agent', className: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300', }, - 'oci-container': { - label: 'OCI', - title: 'OCI Container', - className: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300', - }, }; const DEFAULT_PRESENTATION: WorkloadTypePresentation = { label: 'Unknown', + pluralLabel: 'Unknown', title: 'Unknown workload type', className: 'bg-surface-alt text-base-content', }; @@ -60,13 +61,15 @@ export const normalizeWorkloadTypePresentationKey = ( ): WorkloadTypePresentationKey | null => { const canonical = canonicalizeFrontendResourceType(value); if (!canonical) return null; + // OCI containers are a flavor of system-container, not a distinct workload kind. + // Callers differentiate via IsOCI / osTemplate and override the title at the call site. + if (canonical === 'oci-container') return 'system-container'; if ( canonical === 'vm' || canonical === 'system-container' || canonical === 'app-container' || canonical === 'pod' || - canonical === 'agent' || - canonical === 'oci-container' + canonical === 'agent' ) { return canonical; } @@ -75,7 +78,7 @@ export const normalizeWorkloadTypePresentationKey = ( export const getWorkloadTypePresentation = ( rawType: string | WorkloadType | null | undefined, - overrides?: Partial>, + overrides?: Partial>, ): WorkloadTypePresentation => { const normalized = normalizeWorkloadTypePresentationKey(rawType); const fallbackLabel = @@ -87,6 +90,7 @@ export const getWorkloadTypePresentation = ( return { ...DEFAULT_PRESENTATION, label: fallbackLabel, + pluralLabel: overrides?.pluralLabel || fallbackLabel, title: overrides?.title || fallbackLabel, }; } @@ -95,6 +99,7 @@ export const getWorkloadTypePresentation = ( return { ...base, label: overrides?.label || base.label, + pluralLabel: overrides?.pluralLabel || base.pluralLabel, title: overrides?.title || base.title, }; };