Canonicalize runtime badge tones

This commit is contained in:
rcourtman
2026-05-19 15:33:08 +01:00
parent 4156c66059
commit 00b98cf08e
9 changed files with 84 additions and 34 deletions
@@ -716,7 +716,7 @@ AI runtime.
so client-side family grouping behaves identically against mock fixtures
and live backends that leave `platformType` empty on a subset of
canonical resource types.
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed platform family, not the primary customer-facing label. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union.
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed platform family, not the primary customer-facing label. Identity colour is semantic, not page-local decoration: shared source/platform badges, host identity badges, and container runtime badges must use the shared presentation helpers so Docker remains on the Docker/Podman blue runtime tone, Podman uses its distinct runtime tone, Proxmox PVE remains orange, and those meanings do not drift across table rows, filters, drawers, or platform pages. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union.
The generated host-profile projection also carries runtime platform fallback
metadata for shared explanation and parity with backend normalization, but
frontend primitives must still render host-profile labels through
@@ -258,6 +258,11 @@ regression protection.
8. Render workload row identity directly from the shared canonical workload helper so row selection, hover, and fallback metadata lookup stay aligned with the same workload contract
9. Format infrastructure sensor labels through the shared `frontend-modern/src/utils/textPresentation.ts` presentation helper instead of maintaining a local title-casing implementation in `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts`
10. Extend workload row contract and per-row hot-path derivations through `frontend-modern/src/components/Workloads/guestRowModel.tsx` and `frontend-modern/src/components/Workloads/useGuestRowState.ts`, and extend tooltip-backed row cell presentation through `frontend-modern/src/components/Workloads/GuestRowCells.tsx`, rather than rebuilding column metadata, row identity, cell tooltips, or anomaly correlation inside `frontend-modern/src/components/Workloads/GuestRow.tsx`
Workload runtime badges must consume the shared resource badge
presentation helper rather than carrying local neutral chip classes, so
Docker, Podman, and future container runtimes keep the same identity tones
across Workloads, Docker, and infrastructure surfaces without adding
per-row styling branches to the hot path.
11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx`
Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`.
History cards must let the plot area stretch to the card height instead of
@@ -969,6 +969,11 @@ platform or host-profile identity, such as PVE `ResourceProxmoxMeta.pveVersion`
or `platformData.proxmox.pveVersion`, PBS `version`, or an agent OS report that
resolves to Unraid or Proxmox VE. They must not attach a collector OS version to
a different API-backed platform identity.
Container runtime badges follow that same shared identity boundary:
`frontend-modern/src/utils/resourceBadgePresentation.ts` owns the runtime badge
label and tone mapping for Docker, Podman, and unknown runtimes, and consumers
must render those badges from the shared helper instead of rebuilding local
runtime chips or page-specific colour classes.
Agent-backed storage resources follow the same distinction: `StorageMeta.platform`
may carry appliance presentation context such as `unraid` so the operator can see
what system owns the array, but realtime `platformType` and source filters must
@@ -1,6 +1,7 @@
export type { ResourceBadge } from '@/utils/resourceBadgePresentation';
export {
getContainerRuntimeBadge,
getContainerRuntimeBadgeForRuntime,
getInfrastructureSystemIdentityBadges,
getInfrastructureSystemIdentitySortLabel,
getInfrastructurePlatformBadges,
@@ -42,7 +42,7 @@ import { getGuestColumnStyle } from './guestRowModel';
export function GuestRow(props: GuestRowProps) {
const {
agentVersion,
appContainerRuntimeLabel,
appContainerRuntimeBadge,
clusterName,
contextLabel,
cpuAnomaly,
@@ -238,19 +238,16 @@ export function GuestRow(props: GuestRowProps) {
<td class="px-1.5 sm:px-2 py-0.5 align-middle" data-workload-col="runtime">
<div class="flex justify-center">
<Show
when={appContainerRuntimeLabel()}
when={appContainerRuntimeBadge()}
fallback={
<span class="text-xs text-slate-400" aria-hidden="true">
</span>
}
>
{(label) => (
<span
class="rounded border border-border bg-surface-alt px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted whitespace-nowrap"
title={`${label()} runtime`}
>
{label()}
{(badge) => (
<span class={badge().classes} title={badge().title}>
{badge().label}
</span>
)}
</Show>
@@ -638,7 +638,9 @@ describe('GuestRow', () => {
}),
visibleColumnIds: ['name', 'runtime', 'image'],
});
expect(screen.getByText('Docker')).toBeTruthy();
const runtimeBadge = screen.getByText('Docker');
expect(runtimeBadge).toBeTruthy();
expect(runtimeBadge.className).toContain('bg-sky-100');
});
it('renders Podman runtime chip in the runtime column for Podman-managed app-containers', () => {
@@ -651,7 +653,9 @@ describe('GuestRow', () => {
}),
visibleColumnIds: ['name', 'runtime', 'image'],
});
expect(screen.getByText('Podman')).toBeTruthy();
const runtimeBadge = screen.getByText('Podman');
expect(runtimeBadge).toBeTruthy();
expect(runtimeBadge.className).toContain('bg-violet-100');
});
it('does not render a runtime chip when runtime and platform are both unknown', () => {
@@ -8,6 +8,7 @@ import type { DisplayMetricType } from '@/utils/metricThresholds';
import { buildMetricKey } from '@/utils/metricsKeys';
import { getGuestPowerIndicator, isGuestRunning } from '@/utils/status';
import { getShortImageName, formatBytes } from '@/utils/format';
import { getContainerRuntimeBadgeForRuntime } from '@/utils/resourceBadgePresentation';
import {
getCanonicalWorkloadId,
getWorkloadMetricsKind,
@@ -114,14 +115,9 @@ export function useGuestRowState(props: GuestRowProps) {
return type === 'vm' || type === 'system-container';
});
const appContainerRuntimeLabel = createMemo<string | null>(() => {
const appContainerRuntimeBadge = createMemo(() => {
if (workloadType() !== 'app-container') return null;
const runtime = (props.guest.containerRuntime || '').trim();
const normalized = runtime.toLowerCase();
if (normalized === 'docker') return 'Docker';
if (normalized === 'podman') return 'Podman';
if (runtime) return runtime;
return null;
return getContainerRuntimeBadgeForRuntime(props.guest.containerRuntime);
});
const isOCIContainer = createMemo(() => {
@@ -272,7 +268,7 @@ export function useGuestRowState(props: GuestRowProps) {
);
return {
appContainerRuntimeLabel,
appContainerRuntimeBadge,
cpuAnomaly,
cpuThresholds,
customUrl,
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import {
dedupeResourceBadges,
getContainerRuntimeBadgeForRuntime,
getInfrastructurePlatformBadges,
getInfrastructureSystemIdentityBadges,
getInfrastructureSystemIdentitySortLabel,
@@ -33,6 +34,22 @@ describe('resourceBadgePresentation', () => {
expect(getPlatformBadge('availability')?.label).toBe('Availability');
});
it('returns canonical runtime badge tones for Docker and Podman identities', () => {
const dockerBadge = getContainerRuntimeBadgeForRuntime('docker');
const podmanBadge = getContainerRuntimeBadgeForRuntime('podman');
expect(dockerBadge).toMatchObject({
label: 'Docker',
title: 'Runtime: Docker',
});
expect(dockerBadge?.classes).toContain('bg-sky-100');
expect(podmanBadge).toMatchObject({
label: 'Podman',
title: 'Runtime: Podman',
});
expect(podmanBadge?.classes).toContain('bg-violet-100');
});
it('returns source badges for infrastructure source types', () => {
expect(getSourceBadge('agent')).toMatchObject({ label: 'Agent', title: 'agent' });
expect(getSourceBadge('hybrid')).toMatchObject({ label: 'Hybrid', title: 'hybrid' });
@@ -9,7 +9,11 @@ import {
getAgentHostProfileManifestEntry,
getSourcePlatformManifestEntry,
} from '@/utils/platformSupportManifest';
import { normalizeSourcePlatformKey, type KnownSourcePlatform } from '@/utils/sourcePlatforms';
import {
getSourcePlatformPresentation,
normalizeSourcePlatformKey,
type KnownSourcePlatform,
} from '@/utils/sourcePlatforms';
import { getSourceTypePresentation } from '@/utils/sourceTypePresentation';
import {
canonicalResourceTypeForDisplay,
@@ -28,6 +32,9 @@ const baseBadge =
const typeClasses = 'bg-surface-alt text-base-content';
const availabilityBadgeClasses = 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300';
const dockerRuntimeBadgeClasses = 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-400';
const podmanRuntimeBadgeClasses =
'bg-violet-100 text-violet-700 dark:bg-violet-900 dark:text-violet-300';
const PRIMARY_SYSTEM_SOURCE_PRIORITY: KnownSourcePlatform[] = [
'proxmox-pve',
@@ -581,9 +588,10 @@ const proxmoxLxcDockerBadge = (resource: Resource): ResourceBadge | null => {
return {
label: 'LXC',
classes: `${baseBadge} ${getWorkloadTypePresentation('system-container').className}`,
title: vmid !== null
? `Docker running inside Proxmox LXC ${vmid}`
: 'Docker running inside a Proxmox LXC',
title:
vmid !== null
? `Docker running inside Proxmox LXC ${vmid}`
: 'Docker running inside a Proxmox LXC',
};
};
@@ -689,6 +697,33 @@ export function getInfrastructureSystemTitleBadges(
]);
}
const getContainerRuntimeLabel = (runtime?: string | null): string => {
const raw = (runtime || '').trim();
const normalized = raw.toLowerCase();
if (normalized === 'docker') return 'Docker';
if (normalized === 'podman') return 'Podman';
return raw;
};
const getContainerRuntimeTone = (label: string): string => {
const normalized = label.trim().toLowerCase();
if (normalized === 'docker') {
return getSourcePlatformPresentation('docker')?.tone ?? dockerRuntimeBadgeClasses;
}
if (normalized === 'podman') return podmanRuntimeBadgeClasses;
return typeClasses;
};
export function getContainerRuntimeBadgeForRuntime(runtime?: string | null): ResourceBadge | null {
const label = getContainerRuntimeLabel(runtime);
if (!label) return null;
return {
label,
classes: `${baseBadge} ${getContainerRuntimeTone(label)}`,
title: `Runtime: ${label}`,
};
}
export function getContainerRuntimeBadge(
platformType?: PlatformType,
platformData?: Record<string, unknown> | null,
@@ -696,15 +731,5 @@ export function getContainerRuntimeBadge(
if (platformType !== 'docker' || !platformData) return null;
const docker = (platformData as { docker?: { runtime?: string } } | undefined)?.docker;
const raw = (docker?.runtime || '').trim();
if (!raw) return null;
const normalized = raw.toLowerCase();
const label = normalized === 'podman' ? 'Podman' : normalized === 'docker' ? 'Docker' : raw;
return {
label,
classes: `${baseBadge} ${typeClasses}`,
title: `Runtime: ${label}`,
};
return getContainerRuntimeBadgeForRuntime(docker?.runtime);
}