Pulse/frontend-modern/src/components/Docker/runtimeDisplay.ts
rcourtman 6eb1a10d9b Refactor: Code cleanup and localStorage consolidation
This commit includes comprehensive codebase cleanup and refactoring:

## Code Cleanup
- Remove dead TypeScript code (types/monitoring.ts - 194 lines duplicate)
- Remove unused Go functions (GetClusterNodes, MigratePassword, GetClusterHealthInfo)
- Clean up commented-out code blocks across multiple files
- Remove unused TypeScript exports (helpTextClass, private tag color helpers)
- Delete obsolete test files and components

## localStorage Consolidation
- Centralize all storage keys into STORAGE_KEYS constant
- Update 5 files to use centralized keys:
  * utils/apiClient.ts (AUTH, LEGACY_TOKEN)
  * components/Dashboard/Dashboard.tsx (GUEST_METADATA)
  * components/Docker/DockerHosts.tsx (DOCKER_METADATA)
  * App.tsx (PLATFORMS_SEEN)
  * stores/updates.ts (UPDATES)
- Benefits: Single source of truth, prevents typos, better maintainability

## Previous Work Committed
- Docker monitoring improvements and disk metrics
- Security enhancements and setup fixes
- API refactoring and cleanup
- Documentation updates
- Build system improvements

## Testing
- All frontend tests pass (29 tests)
- All Go tests pass (15 packages)
- Production build successful
- Zero breaking changes

Total: 186 files changed, 5825 insertions(+), 11602 deletions(-)
2025-11-04 21:50:46 +00:00

196 lines
5.7 KiB
TypeScript

import type { DockerContainer, DockerHost } from '@/types/api';
export type RuntimeKind = 'docker' | 'podman' | 'containerd' | 'cri-o' | 'nerdctl' | 'unknown';
export interface RuntimeDisplayInfo {
id: RuntimeKind;
label: string;
badgeClass: string;
raw: string;
}
export interface RuntimeDisplayOptions {
hint?: string | null;
defaultId?: Exclude<RuntimeKind, 'unknown'> | 'unknown';
}
const BADGE_CLASSES: Record<Exclude<RuntimeKind, 'unknown'>, string> = {
docker: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
podman: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
containerd: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
'cri-o': 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
nerdctl: 'bg-slate-200 text-slate-700 dark:bg-slate-700 dark:text-slate-200',
};
const UNKNOWN_BADGE_CLASS = 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300';
const LABEL_BY_KIND: Record<RuntimeKind, string> = {
docker: 'Docker',
podman: 'Podman',
containerd: 'containerd',
'cri-o': 'CRI-O',
nerdctl: 'nerdctl',
unknown: 'Container runtime',
};
const normalize = (value?: string | null) => value?.trim() ?? '';
const MATCHERS: Array<{ id: Exclude<RuntimeKind, 'unknown'>; keywords: string[] }> = [
{ id: 'podman', keywords: ['podman', 'libpod'] },
{ id: 'nerdctl', keywords: ['nerdctl'] },
{ id: 'containerd', keywords: ['containerd'] },
{ id: 'cri-o', keywords: ['cri-o', 'crio'] },
{ id: 'docker', keywords: ['docker', 'moby engine', 'moby'] },
];
const detectRuntime = (value?: string | null) => {
const raw = normalize(value);
if (!raw) {
return null;
}
const normalized = raw.toLowerCase();
for (const matcher of MATCHERS) {
if (matcher.keywords.some((keyword) => normalized.includes(keyword))) {
return { id: matcher.id, raw };
}
}
return null;
};
const formatRuntimeLabel = (value: string): string => {
if (!value) return LABEL_BY_KIND.unknown;
const cleaned = value.replace(/\s+/g, ' ').trim();
if (/^cri-?o$/i.test(cleaned)) {
return LABEL_BY_KIND['cri-o'];
}
return cleaned
.split(/[-_\s]+/)
.filter(Boolean)
.map((segment) => {
if (segment.toLowerCase() === 'cri' || segment.toLowerCase() === 'crio') {
return 'CRI';
}
if (segment.toLowerCase() === 'o') {
return 'O';
}
if (segment === segment.toUpperCase()) {
return segment;
}
return segment.charAt(0).toUpperCase() + segment.slice(1);
})
.join(' ');
};
export const getRuntimeDisplay = (
runtime?: string | null,
options: RuntimeDisplayOptions = {},
): RuntimeDisplayInfo => {
const candidates = [
{ value: runtime, raw: normalize(runtime) },
{ value: options.hint, raw: normalize(options.hint) },
];
for (const candidate of candidates) {
const detected = detectRuntime(candidate.value);
if (detected) {
const label = LABEL_BY_KIND[detected.id];
const badgeClass = BADGE_CLASSES[detected.id];
return {
id: detected.id,
label,
badgeClass,
raw: detected.raw,
};
}
}
const firstRaw = candidates.find((candidate) => candidate.raw !== '')?.raw ?? '';
const defaultKind: RuntimeKind = options.defaultId ?? 'unknown';
if (defaultKind !== 'unknown') {
return {
id: defaultKind,
label: LABEL_BY_KIND[defaultKind],
badgeClass: BADGE_CLASSES[defaultKind],
raw: firstRaw,
};
}
const label = firstRaw ? formatRuntimeLabel(firstRaw) : LABEL_BY_KIND.unknown;
return {
id: 'unknown',
label,
badgeClass: UNKNOWN_BADGE_CLASS,
raw: firstRaw,
};
};
const PODMAN_LABEL_PREFIXES = ['io.podman.', 'io.containers.', 'net.containers.podman'];
const hasPodmanSignature = (container?: DockerContainer | null) => {
if (!container?.labels) return false;
const labels = container.labels;
return Object.keys(labels).some((key) =>
PODMAN_LABEL_PREFIXES.some((prefix) => key.startsWith(prefix)),
) || Object.values(labels).some((value) => value?.toLowerCase().includes('podman'));
};
const hostHasPodmanSignature = (host?: Pick<DockerHost, 'containers'> | null) => {
if (!host?.containers || host.containers.length === 0) return false;
return host.containers.some((container) => hasPodmanSignature(container));
};
export interface ResolveRuntimeOptions extends RuntimeDisplayOptions {
requireSignatureForGuess?: boolean;
}
const stripVersion = (value?: string | null) => {
if (!value) return '';
const match = value.match(/\d+(?:\.\d+){0,2}/);
return match ? match[0] : value.trim();
};
const isLikelyPodmanVersion = (value?: string | null) => {
const clean = stripVersion(value);
if (!clean) return false;
const parts = clean.split('.');
const major = Number.parseInt(parts[0] || '', 10);
if (!Number.isFinite(major)) return false;
return major >= 2 && major <= 6;
};
export const resolveHostRuntime = (
host: Pick<DockerHost, 'runtime' | 'runtimeVersion' | 'dockerVersion' | 'containers'>,
options: ResolveRuntimeOptions = {},
): RuntimeDisplayInfo => {
const hint = options.hint ?? host.runtimeVersion ?? host.dockerVersion ?? null;
const base = getRuntimeDisplay(host.runtime, {
...options,
hint,
defaultId: options.defaultId ?? 'docker',
});
if (base.id !== 'docker' && base.id !== 'unknown') {
return base;
}
const hasSignature = hostHasPodmanSignature(host);
if (hasSignature) {
return getRuntimeDisplay('podman', { hint });
}
if (!options.requireSignatureForGuess && isLikelyPodmanVersion(hint)) {
return getRuntimeDisplay('podman', { hint });
}
if (base.id === 'unknown') {
return getRuntimeDisplay('docker', { hint });
}
return base;
};