mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-12 07:18:49 +00:00
Move workload infrastructure links into routing owner
This commit is contained in:
@@ -156,6 +156,11 @@ and the canonical row contract and per-row hot-path derivations live in
|
||||
identity, column, cell-tooltip, anomaly-correlation, and link-state changes
|
||||
must extend through those owners instead of rebuilding row-local state inside
|
||||
the shell.
|
||||
That per-row link state now also consumes the shared
|
||||
`frontend-modern/src/routing/resourceLinks.ts` workload-to-infrastructure
|
||||
helper instead of a dashboard-local routing shim. Future infrastructure-link
|
||||
changes for dashboard rows must extend through the shared routing owner rather
|
||||
than recreating feature-local path builders.
|
||||
That shell now also routes tag-dot rendering through the shared
|
||||
`frontend-modern/src/components/shared/TagBadges.tsx` primitive instead of
|
||||
keeping a dashboard-local badge helper. Future guest-row tag presentation
|
||||
@@ -171,6 +176,10 @@ and workload-derived navigation state live in
|
||||
`frontend-modern/src/components/Dashboard/useGuestDrawerState.ts`. Future
|
||||
drawer runtime and overview-surface changes must extend through those owners
|
||||
instead of adding more mixed state and helper drift back into the shell.
|
||||
That drawer state now also consumes the same shared
|
||||
`frontend-modern/src/routing/resourceLinks.ts` workload-to-infrastructure
|
||||
helper, so row and drawer navigation stay aligned without a second
|
||||
dashboard-local link-mapping file.
|
||||
The dashboard disk list now follows the same pattern: the shell stays in
|
||||
`frontend-modern/src/components/Dashboard/DiskList.tsx`, while disk-row
|
||||
presentation derivations and fallback tooltip/runtime wiring live in
|
||||
|
||||
@@ -260,6 +260,12 @@ The frontend now also consumes those facet reads through
|
||||
`frontend-modern/src/api/resources.ts` and the dedicated resource detail
|
||||
drawer, which keeps the presentation surface aligned with the governed API
|
||||
contract instead of rebuilding the relationship and timeline inline.
|
||||
The canonical routing owner now also lives in
|
||||
`frontend-modern/src/routing/resourceLinks.ts`, including the
|
||||
workload-to-infrastructure href builder used by dashboard row and drawer
|
||||
consumers. Future workload-to-resource navigation changes must extend through
|
||||
that shared routing contract instead of reintroducing dashboard-local path
|
||||
builders.
|
||||
That drawer shell now routes its canonical timeline filter, facet-bundle, and
|
||||
resource-intelligence state through
|
||||
`frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts`,
|
||||
|
||||
+4
@@ -600,6 +600,8 @@ describe('Dashboard performance contract', () => {
|
||||
expect(guestRowModelSource).toContain('export const VIEW_MODE_COLUMNS');
|
||||
expect(guestRowStateSource).toContain('getCanonicalWorkloadId');
|
||||
expect(guestRowStateSource).toContain('buildMetricKey');
|
||||
expect(guestRowStateSource).toContain("from '@/routing/resourceLinks'");
|
||||
expect(guestRowStateSource).not.toContain("./infrastructureLink");
|
||||
expect(guestRowStateSource).toContain('getWorkloadTypeBadge');
|
||||
expect(guestRowCellsSource).toContain('export { BackupIndicator');
|
||||
expect(guestRowCellsSource).toContain('function NetworkInfoCell(');
|
||||
@@ -616,6 +618,8 @@ describe('Dashboard performance contract', () => {
|
||||
expect(guestDrawerSource).not.toContain('WebInterfaceUrlField');
|
||||
expect(guestDrawerStateSource).toContain('getCanonicalWorkloadId');
|
||||
expect(guestDrawerStateSource).toContain('buildInfrastructureHrefForWorkload');
|
||||
expect(guestDrawerStateSource).toContain("from '@/routing/resourceLinks'");
|
||||
expect(guestDrawerStateSource).not.toContain("./infrastructureLink");
|
||||
expect(guestDrawerStateSource).toContain('getDiscoveryResourceTypeForWorkload');
|
||||
expect(guestDrawerStateSource).toContain('getWebInterfaceTargetLabelForWorkload');
|
||||
expect(guestDrawerStateSource).toContain('guestOsSummary');
|
||||
|
||||
@@ -99,9 +99,15 @@ vi.mock('@/components/shared/workloadTypeBadges', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../infrastructureLink', () => ({
|
||||
buildInfrastructureHrefForWorkload: () => '/infrastructure/node1',
|
||||
}));
|
||||
vi.mock('@/routing/resourceLinks', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/routing/resourceLinks')>(
|
||||
'@/routing/resourceLinks',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
buildInfrastructureHrefForWorkload: () => '/infrastructure/node1',
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../workloadSelectors', () => ({
|
||||
getWorkloadDockerHostId: (guest: WorkloadGuest) => guest.dockerHostId || '',
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { WorkloadGuest } from '@/types/workloads';
|
||||
import { buildInfrastructureHrefForWorkload } from '@/components/Dashboard/infrastructureLink';
|
||||
|
||||
const baseGuest = (overrides: Partial<WorkloadGuest>): WorkloadGuest => ({
|
||||
id: 'guest-1',
|
||||
vmid: 101,
|
||||
name: 'guest-1',
|
||||
node: 'node-1',
|
||||
instance: 'cluster-a',
|
||||
status: 'running',
|
||||
type: 'vm',
|
||||
cpu: 0,
|
||||
cpus: 2,
|
||||
memory: { total: 0, used: 0, free: 0, usage: 0 },
|
||||
disk: { total: 0, used: 0, free: 0, usage: 0 },
|
||||
networkIn: 0,
|
||||
networkOut: 0,
|
||||
diskRead: 0,
|
||||
diskWrite: 0,
|
||||
uptime: 0,
|
||||
template: false,
|
||||
lastBackup: 0,
|
||||
tags: [],
|
||||
lock: '',
|
||||
lastSeen: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('buildInfrastructureHrefForWorkload', () => {
|
||||
it('maps vm workloads to proxmox infrastructure source with node query', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'vm',
|
||||
workloadType: 'vm',
|
||||
node: 'pve1',
|
||||
instance: 'cluster-main',
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=proxmox-pve&q=pve1');
|
||||
});
|
||||
|
||||
it('maps app-container workloads to docker infrastructure source with context query', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
contextLabel: 'docker-host-1',
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=docker&q=docker-host-1');
|
||||
});
|
||||
|
||||
it('maps pod workloads to kubernetes infrastructure source with cluster query', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'pod',
|
||||
workloadType: 'pod',
|
||||
contextLabel: 'cluster-a',
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=kubernetes&q=cluster-a');
|
||||
});
|
||||
|
||||
it('defaults unknown workload types to proxmox mapping (legacy compatibility)', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'unknown',
|
||||
workloadType: undefined,
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=proxmox-pve&q=node-1');
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { WorkloadGuest } from '@/types/workloads';
|
||||
import { buildInfrastructurePath } from '@/routing/resourceLinks';
|
||||
import { resolveWorkloadType } from '@/utils/workloads';
|
||||
|
||||
const firstNonEmpty = (values: Array<string | undefined | null>): string | undefined => {
|
||||
for (const value of values) {
|
||||
if (typeof value !== 'string') continue;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length > 0) return trimmed;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const buildInfrastructureHrefForWorkload = (guest: WorkloadGuest): string => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
|
||||
if (type === 'vm' || type === 'system-container') {
|
||||
const query = firstNonEmpty([guest.node, guest.instance, guest.name]);
|
||||
return buildInfrastructurePath({ source: 'proxmox-pve', query });
|
||||
}
|
||||
|
||||
if (type === 'app-container') {
|
||||
const query = firstNonEmpty([guest.contextLabel, guest.node, guest.instance, guest.name]);
|
||||
return buildInfrastructurePath({ source: 'docker', query });
|
||||
}
|
||||
|
||||
if (type === 'pod') {
|
||||
const query = firstNonEmpty([
|
||||
guest.contextLabel,
|
||||
guest.instance,
|
||||
guest.namespace,
|
||||
guest.node,
|
||||
guest.name,
|
||||
]);
|
||||
return buildInfrastructurePath({ source: 'kubernetes', query });
|
||||
}
|
||||
|
||||
return buildInfrastructurePath();
|
||||
};
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
getDiscoveryResourceTypeForWorkload,
|
||||
getWebInterfaceTargetLabelForWorkload,
|
||||
} from '@/utils/workloads';
|
||||
import { buildInfrastructureHrefForWorkload } from '@/routing/resourceLinks';
|
||||
|
||||
import { buildInfrastructureHrefForWorkload } from './infrastructureLink';
|
||||
import {
|
||||
getDiscoveryHostIdForWorkload,
|
||||
getDiscoveryResourceIdForWorkload,
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
getWorkloadMetricsKind,
|
||||
resolveWorkloadType,
|
||||
} from '@/utils/workloads';
|
||||
import { buildInfrastructureHrefForWorkload } from '@/routing/resourceLinks';
|
||||
import { getWorkloadTypeBadge } from '@/components/shared/workloadTypeBadges';
|
||||
|
||||
import { buildInfrastructureHrefForWorkload } from './infrastructureLink';
|
||||
import { getWorkloadDockerHostId } from './workloadSelectors';
|
||||
import {
|
||||
DEFAULT_FIRST_CELL_INDENT,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { WorkloadGuest } from '@/types/workloads';
|
||||
import {
|
||||
PMG_THRESHOLDS_PATH,
|
||||
RECOVERY_QUERY_PARAMS,
|
||||
buildInfrastructureHrefForWorkload,
|
||||
buildRecoveryPath,
|
||||
buildInfrastructurePath,
|
||||
buildInfrastructureResourceHref,
|
||||
@@ -16,6 +18,31 @@ import {
|
||||
WORKLOADS_QUERY_PARAMS,
|
||||
} from '@/routing/resourceLinks';
|
||||
|
||||
const baseGuest = (overrides: Partial<WorkloadGuest>): WorkloadGuest => ({
|
||||
id: 'guest-1',
|
||||
vmid: 101,
|
||||
name: 'guest-1',
|
||||
node: 'node-1',
|
||||
instance: 'cluster-a',
|
||||
status: 'running',
|
||||
type: 'vm',
|
||||
cpu: 0,
|
||||
cpus: 2,
|
||||
memory: { total: 0, used: 0, free: 0, usage: 0 },
|
||||
disk: { total: 0, used: 0, free: 0, usage: 0 },
|
||||
networkIn: 0,
|
||||
networkOut: 0,
|
||||
diskRead: 0,
|
||||
diskWrite: 0,
|
||||
uptime: 0,
|
||||
template: false,
|
||||
lastBackup: 0,
|
||||
tags: [],
|
||||
lock: '',
|
||||
lastSeen: new Date().toISOString(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('resource link routing contract', () => {
|
||||
it('builds and parses workloads query params', () => {
|
||||
const href = buildWorkloadsPath({
|
||||
@@ -93,6 +120,50 @@ describe('resource link routing contract', () => {
|
||||
expect(buildInfrastructureResourceHref('')).toBeNull();
|
||||
});
|
||||
|
||||
it('maps vm workloads to proxmox infrastructure source with node query', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'vm',
|
||||
workloadType: 'vm',
|
||||
node: 'pve1',
|
||||
instance: 'cluster-main',
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=proxmox-pve&q=pve1');
|
||||
});
|
||||
|
||||
it('maps app-container workloads to docker infrastructure source with context query', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
contextLabel: 'docker-host-1',
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=docker&q=docker-host-1');
|
||||
});
|
||||
|
||||
it('maps pod workloads to kubernetes infrastructure source with cluster query', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'pod',
|
||||
workloadType: 'pod',
|
||||
contextLabel: 'cluster-a',
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=kubernetes&q=cluster-a');
|
||||
});
|
||||
|
||||
it('defaults unknown workload types to proxmox infrastructure compatibility mapping', () => {
|
||||
const href = buildInfrastructureHrefForWorkload(
|
||||
baseGuest({
|
||||
type: 'unknown',
|
||||
workloadType: undefined,
|
||||
}),
|
||||
);
|
||||
expect(href).toBe('/infrastructure?source=proxmox-pve&q=node-1');
|
||||
});
|
||||
|
||||
it('builds and parses storage query params', () => {
|
||||
const href = buildStoragePath({
|
||||
tab: 'disks',
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { normalizeSourcePlatformQueryValue } from '@/utils/sourcePlatforms';
|
||||
import { normalizeStorageSourceKey } from '@/utils/storageSources';
|
||||
import { canonicalizeWorkloadFilterType } from '@/utils/workloads';
|
||||
import {
|
||||
canonicalizeWorkloadFilterType,
|
||||
resolveWorkloadType,
|
||||
} from '@/utils/workloads';
|
||||
import type { WorkloadGuest } from '@/types/workloads';
|
||||
|
||||
export const WORKLOADS_QUERY_PARAMS = {
|
||||
type: 'type',
|
||||
@@ -67,6 +71,15 @@ const normalizeQueryBooleanFlag = (value: string | null | undefined): string =>
|
||||
const normalizeWorkloadsType = (value: string | null | undefined): string =>
|
||||
canonicalizeWorkloadFilterType(normalizeQueryValue(value));
|
||||
|
||||
const firstNonEmpty = (values: Array<string | undefined | null>): string | undefined => {
|
||||
for (const value of values) {
|
||||
if (typeof value !== 'string') continue;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length > 0) return trimmed;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type WorkloadsLinkOptions = {
|
||||
type?: string | null;
|
||||
runtime?: string | null;
|
||||
@@ -166,6 +179,33 @@ export const buildInfrastructureResourceHref = (resourceId: string): string | nu
|
||||
return trimmed ? buildInfrastructurePath({ resource: trimmed }) : null;
|
||||
};
|
||||
|
||||
export const buildInfrastructureHrefForWorkload = (guest: WorkloadGuest): string => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
|
||||
if (type === 'vm' || type === 'system-container') {
|
||||
const query = firstNonEmpty([guest.node, guest.instance, guest.name]);
|
||||
return buildInfrastructurePath({ source: 'proxmox-pve', query });
|
||||
}
|
||||
|
||||
if (type === 'app-container') {
|
||||
const query = firstNonEmpty([guest.contextLabel, guest.node, guest.instance, guest.name]);
|
||||
return buildInfrastructurePath({ source: 'docker', query });
|
||||
}
|
||||
|
||||
if (type === 'pod') {
|
||||
const query = firstNonEmpty([
|
||||
guest.contextLabel,
|
||||
guest.instance,
|
||||
guest.namespace,
|
||||
guest.node,
|
||||
guest.name,
|
||||
]);
|
||||
return buildInfrastructurePath({ source: 'kubernetes', query });
|
||||
}
|
||||
|
||||
return buildInfrastructurePath();
|
||||
};
|
||||
|
||||
export const parseStorageLinkSearch = (search: string) => {
|
||||
const params = new URLSearchParams(search);
|
||||
return {
|
||||
|
||||
@@ -470,6 +470,7 @@ describe('frontend resource type boundaries', () => {
|
||||
it('keeps compatibility handling centralized in shared adapters and edge translators', () => {
|
||||
expect(resourceLinksSource).toContain('canonicalizeWorkloadFilterType');
|
||||
expect(resourceLinksSource).toContain('normalizeSourcePlatformQueryValue');
|
||||
expect(resourceLinksSource).toContain('export const buildInfrastructureHrefForWorkload');
|
||||
expect(resourceLinksSource).not.toContain("normalized === 'docker'");
|
||||
expect(resourceLinksSource).not.toContain("normalized === 'k8s'");
|
||||
expect(sourcePlatformsSource).toContain('export const normalizeSourcePlatformQueryValue');
|
||||
@@ -604,6 +605,8 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(guestRowSource).not.toContain('function NetworkInfoCell(');
|
||||
expect(guestRowSource).not.toContain('function OSInfoCell(');
|
||||
expect(guestRowStateSource).toContain('getCanonicalWorkloadId');
|
||||
expect(guestRowStateSource).toContain("from '@/routing/resourceLinks'");
|
||||
expect(guestRowStateSource).not.toContain("./infrastructureLink");
|
||||
expect(guestRowModelSource).toContain('export const GUEST_COLUMNS');
|
||||
expect(guestRowCellsSource).toContain('function NetworkInfoCell(');
|
||||
expect(guestRowCellsSource).toContain('function OSInfoCell(');
|
||||
@@ -617,6 +620,8 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(guestDrawerSource).toContain('useGuestDrawerState');
|
||||
expect(guestDrawerSource).toContain('GuestDrawerOverview');
|
||||
expect(guestDrawerStateSource).toContain('getCanonicalWorkloadId');
|
||||
expect(guestDrawerStateSource).toContain("from '@/routing/resourceLinks'");
|
||||
expect(guestDrawerStateSource).not.toContain("./infrastructureLink");
|
||||
expect(guestDrawerStateSource).toContain('guestOsSummary');
|
||||
expect(guestDrawerSource).not.toContain('const guestId = () => {');
|
||||
expect(guestDrawerSource).not.toContain('WebInterfaceUrlField');
|
||||
|
||||
Reference in New Issue
Block a user