mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Canonicalize runtime badge tones
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user