From ac0204283aae8a47348c0e9190ba37e7392fbb53 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 23 Mar 2026 10:57:22 +0000 Subject: [PATCH] Extract resource drawer discovery config owner --- .../subsystems/performance-and-scalability.md | 8 + .../v6/internal/subsystems/registry.json | 3 + .../internal/subsystems/unified-resources.md | 35 +-- .../ResourceDetailDrawer.discovery.test.ts | 2 +- .../ResourceDetailDrawer.history.test.tsx | 6 +- ...esourceTable.performance.contract.test.tsx | 5 + .../__tests__/resourceDetailMappers.test.ts | 45 ---- .../resourceDetailDiscoveryModel.ts | 235 ++++++++++++++++++ .../Infrastructure/resourceDetailMappers.ts | 203 +-------------- .../useResourceDetailDrawerDerivedState.ts | 2 +- .../frontendResourceTypeBoundaries.test.ts | 9 +- .../canonical_completion_guard_test.py | 1 + .../release_control/subsystem_lookup_test.py | 31 +++ 13 files changed, 321 insertions(+), 264 deletions(-) create mode 100644 frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index e1d8b93c6..62f340fc5 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -253,6 +253,14 @@ 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 shared infrastructure mapper hot path now stays intentionally narrow: +`frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` +continues to own sensor-label presentation and hot-path host/agent projection, +while canonical drawer discovery-target derivation now lives in +`frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts` +under `unified-resources`. Future discovery-config or target-resolution +changes must extend through that unified-resource owner instead of +re-accumulating discovery heuristics back into the performance hot-path mapper. 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 diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 97c07990a..bc571bd55 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -3206,6 +3206,7 @@ "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts", "frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx", "frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts", + "frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts", "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx", "frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts", @@ -3301,6 +3302,7 @@ "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts", "frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx", "frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts", + "frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts", "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", "frontend-modern/src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx", "frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts", @@ -3337,6 +3339,7 @@ "test_prefixes": [], "exact_files": [ "frontend-modern/src/components/Infrastructure/__tests__/infrastructureSelectors.test.ts", + "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts", "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx", "frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerOperationalModel.test.ts", "frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts", diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 05c97196e..0a5dbab21 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -55,20 +55,22 @@ cross-source deduplication. 33. `frontend-modern/src/components/Infrastructure/unifiedResourceTableModel.ts` 34. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts` 35. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerServiceModel.ts` -36. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts` -37. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts` -38. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts` -39. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` -40. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts` -41. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` -42. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts` -43. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx` -44. `frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts` -45. `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts` -46. `frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx` -47. `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts` -48. `frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts` -49. `frontend-modern/src/utils/resourceIdentity.ts` +36. `frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts` +37. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts` +38. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts` +39. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts` +40. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerState.ts` +41. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` +42. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableViewportSync.ts` +43. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` +44. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts` +45. `frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx` +46. `frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts` +47. `frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts` +48. `frontend-modern/src/components/Infrastructure/InfrastructureSummary.tsx` +49. `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts` +50. `frontend-modern/src/components/Infrastructure/infrastructureSummaryModel.ts` +51. `frontend-modern/src/utils/resourceIdentity.ts` ## Shared Boundaries @@ -164,6 +166,11 @@ split used elsewhere in v6: `ResourceDetailDrawer.tsx` owns composition, `useResourceDetailDrawerHistoryState.ts` owns facet/intelligence/timeline runtime orchestration, `useResourceDetailDrawerDerivedState.ts` owns the canonical drawer derivation layer, and +`resourceDetailDiscoveryModel.ts` owns the pure canonical discovery-config +derivation that feeds the drawer discovery surface, +`resourceDetailDrawerOperationalModel.ts` owns the pure source-health, +platform-signal, related-link, and host-detail overview derivations that feed +the current-state and host-details surfaces, `resourceDetailDrawerServiceModel.ts` owns the pure Docker/PBS/PMG service summary and breakdown derivations that feed the overview service-details surface, diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts index 1cbf747a9..870919ec6 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { Resource } from '@/types/resource'; -import { toDiscoveryConfig } from '@/components/Infrastructure/resourceDetailMappers'; +import { toDiscoveryConfig } from '@/components/Infrastructure/resourceDetailDiscoveryModel'; import { buildWorkloadsHref } from '@/components/Infrastructure/workloadsLink'; const baseResource = (): Resource => ({ diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index e83900aad..d18b2e902 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -7,6 +7,7 @@ import resourceDetailDrawerShellSource from '@/components/Infrastructure/Resourc import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw'; import resourceDetailDrawerHistoryStateSource from '@/components/Infrastructure/useResourceDetailDrawerHistoryState.ts?raw'; import resourceDetailDrawerDerivedStateSource from '@/components/Infrastructure/useResourceDetailDrawerDerivedState.ts?raw'; +import resourceDetailDrawerDiscoveryModelSource from '@/components/Infrastructure/resourceDetailDiscoveryModel.ts?raw'; import resourceDetailDrawerServiceModelSource from '@/components/Infrastructure/resourceDetailDrawerServiceModel.ts?raw'; import resourceDetailDrawerDockerActionsStateSource from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts?raw'; import resourceDetailDrawerStateSource from '@/components/Infrastructure/useResourceDetailDrawerState.ts?raw'; @@ -134,11 +135,14 @@ describe('ResourceDetailDrawer change history section', () => { expect(resourceDetailDrawerHistoryStateSource).toContain('createResource('); expect(resourceDetailDrawerHistoryStateSource).toContain('ResourceAPI.getFacetBundle'); expect(resourceDetailDrawerHistoryStateSource).toContain('AIAPI.getResourceIntelligence'); - expect(resourceDetailDrawerDerivedStateSource).toContain('buildWorkloadsHref'); expect(resourceDetailDrawerDerivedStateSource).toContain('toDiscoveryConfig'); + expect(resourceDetailDrawerDerivedStateSource).toContain( + "from '@/components/Infrastructure/resourceDetailDiscoveryModel'", + ); expect(resourceDetailDrawerDerivedStateSource).toContain( "from './resourceDetailDrawerServiceModel'", ); + expect(resourceDetailDrawerDiscoveryModelSource).toContain('export const toDiscoveryConfig'); expect(resourceDetailDrawerServiceModelSource).toContain('export const getServiceDetailsSummary'); expect(resourceDetailDrawerServiceModelSource).toContain( 'export const buildPbsVisibleJobBreakdown', diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx index 8593b8f98..e01ef2cd4 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx @@ -12,6 +12,8 @@ import unifiedResourceTableModelSource from '@/components/Infrastructure/unified import infrastructureSummarySource from '@/components/Infrastructure/InfrastructureSummary.tsx?raw'; import infrastructureSummaryStateSource from '@/components/Infrastructure/useInfrastructureSummaryState.ts?raw'; import infrastructureSummaryModelSource from '@/components/Infrastructure/infrastructureSummaryModel.ts?raw'; +import resourceDetailMappersSource from '@/components/Infrastructure/resourceDetailMappers.ts?raw'; +import resourceDetailDiscoveryModelSource from '@/components/Infrastructure/resourceDetailDiscoveryModel.ts?raw'; import { buildStatusOptions, filterResources, @@ -180,6 +182,9 @@ describe('UnifiedResourceTable performance contract', () => { it('formats sensor labels through the shared resource detail mapper helper', () => { expect(formatSensorName('fan1_cpu_temp')).toBe('Cpu Temp'); expect(formatSensorName('psu_temp')).toBe('Temp'); + expect(resourceDetailMappersSource).toContain('titleCaseDelimitedLabel'); + expect(resourceDetailMappersSource).not.toContain('export const toDiscoveryConfig'); + expect(resourceDetailDiscoveryModelSource).toContain('export const toDiscoveryConfig'); }); it('keeps hot-path table state and windowing in the shared table state owner', () => { diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts index 8499d9b81..2763aae24 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailMappers.test.ts @@ -3,7 +3,6 @@ import { formatInteger, formatSensorName, formatSourceType, - toDiscoveryConfig, toAgentFromResource, toNodeFromProxmox, } from '@/components/Infrastructure/resourceDetailMappers'; @@ -112,48 +111,4 @@ describe('resourceDetailMappers', () => { expect(agent?.id).not.toBe('resource:host:hash-1'); }); }); - - describe('toDiscoveryConfig', () => { - it('prefers the typed canonical hostname for explicit discovery targets', () => { - const config = toDiscoveryConfig({ - ...createHybridHostResource(), - displayName: '', - canonicalIdentity: { - hostname: 'tower.canonical', - displayName: 'Tower', - primaryId: 'node:instance-pve1', - }, - discoveryTarget: { - resourceType: 'agent', - agentId: 'agent-canonical', - resourceId: 'agent-canonical', - }, - }); - - expect(config?.hostname).toBe('tower.canonical'); - }); - - it('uses the shared Kubernetes cluster helper for pod fallback agent ids', () => { - const config = toDiscoveryConfig({ - ...createHybridHostResource(), - id: 'resource:pod:hash-2', - type: 'pod', - platformType: 'kubernetes', - kubernetes: { - context: 'cluster-context', - podUid: 'pod-uid-2', - namespace: 'default', - }, - platformData: { - kubernetes: { - context: 'cluster-context', - namespace: 'default', - }, - }, - } as unknown as Resource); - - expect(config?.agentId).toBe('cluster-context'); - expect(config?.resourceId).toBe('pod-uid-2'); - }); - }); }); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts new file mode 100644 index 000000000..f68b4df0c --- /dev/null +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts @@ -0,0 +1,235 @@ +import type { Resource } from '@/types/resource'; +import type { ResourceType as DiscoveryResourceType } from '@/types/discovery'; +import { + canonicalDiscoveryResourceType, + isAgentDiscoveryResourceType, +} from '@/utils/discoveryTarget'; +import { + getActionableAgentIdFromResource, + getActionableDockerRuntimeIdFromResource, + getActionableKubernetesClusterIdFromResource, +} from '@/utils/agentResources'; +import { + getPreferredResourceClusterName, + getPreferredResourceDisplayName, + getPreferredResourceHostname, +} from '@/utils/resourceIdentity'; + +export type DiscoveryConfig = { + resourceType: DiscoveryResourceType; + agentId: string; + resourceId: string; + hostname: string; + metadataKind: 'guest' | 'agent'; + metadataId: string; + targetLabel: string; +}; + +type ProxmoxPlatformData = { + nodeName?: string; + vmid?: number; +}; + +type DockerPlatformData = { + containerId?: string; + hostname?: string; +}; + +type KubernetesPlatformData = { + agentId?: string; + namespace?: string; + podName?: string; + podUid?: string; +}; + +type PlatformData = { + agent?: { + hostname?: string; + }; + docker?: DockerPlatformData; + kubernetes?: KubernetesPlatformData; + proxmox?: ProxmoxPlatformData; +}; + +const asString = (value: unknown): string | undefined => + typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; + +const asNumber = (value: unknown): number | undefined => + typeof value === 'number' && Number.isFinite(value) ? value : undefined; + +const getPreferredHostLabel = (resource: Resource): string => + getPreferredResourceHostname(resource) || + getPreferredResourceDisplayName(resource) || + resource.id; + +export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null => { + const explicitDiscoveryTarget = resource.discoveryTarget; + const explicitDiscoveryAgentId = asString( + (explicitDiscoveryTarget as { agentId?: unknown } | undefined)?.agentId, + ); + + if ( + explicitDiscoveryTarget && + explicitDiscoveryTarget.resourceType && + explicitDiscoveryAgentId && + explicitDiscoveryTarget.resourceId + ) { + const explicitResourceType = canonicalDiscoveryResourceType( + explicitDiscoveryTarget.resourceType, + ); + const resourceType = (() => { + switch (explicitResourceType) { + case 'agent': + return 'agent'; + case 'vm': + case 'system-container': + case 'app-container': + case 'pod': + return explicitResourceType; + default: + return null; + } + })(); + + if (resourceType) { + const hostname = explicitDiscoveryTarget.hostname || getPreferredHostLabel(resource); + const isHostDiscovery = isAgentDiscoveryResourceType(resourceType); + const targetLabel = isHostDiscovery + ? 'agent' + : resourceType === 'app-container' + ? 'container' + : resourceType === 'pod' + ? 'workload' + : 'guest'; + return { + resourceType, + agentId: explicitDiscoveryAgentId, + resourceId: explicitDiscoveryTarget.resourceId, + hostname, + metadataKind: isHostDiscovery ? 'agent' : 'guest', + metadataId: explicitDiscoveryTarget.resourceId, + targetLabel, + }; + } + } + + const platformData = resource.platformData as PlatformData | undefined; + const dockerPlatformData = platformData?.docker; + const kubernetesPlatformData = platformData?.kubernetes; + const proxmoxVmid = + asNumber(resource.proxmox?.vmid) ?? + asNumber(platformData?.proxmox?.vmid) ?? + asNumber((platformData as { vmid?: unknown } | undefined)?.vmid); + const vmidResourceId = + proxmoxVmid !== undefined && proxmoxVmid > 0 ? String(proxmoxVmid) : undefined; + const proxmoxNodeName = + asString(resource.proxmox?.nodeName) || + platformData?.proxmox?.nodeName || + asString((platformData as { nodeName?: unknown } | undefined)?.nodeName); + const actionableAgentId = getActionableAgentIdFromResource(resource); + const actionableDockerHostId = getActionableDockerRuntimeIdFromResource(resource); + const actionableKubernetesId = getActionableKubernetesClusterIdFromResource(resource); + const kubernetesAgentId = + asString(resource.kubernetes?.agentId) || + asString(kubernetesPlatformData?.agentId) || + actionableKubernetesId || + getPreferredResourceClusterName(resource); + const kubernetesResourceId = + asString(resource.kubernetes?.podUid) || + asString(kubernetesPlatformData?.podUid) || + (() => { + const namespace = + asString(resource.kubernetes?.namespace) || asString(kubernetesPlatformData?.namespace); + const podName = + asString(resource.kubernetes?.podName) || + asString(kubernetesPlatformData?.podName) || + asString(resource.name); + return namespace && podName ? `${namespace}/${podName}` : undefined; + })(); + const agentLookupId = + actionableDockerHostId || + actionableKubernetesId || + actionableAgentId || + proxmoxNodeName || + platformData?.agent?.hostname || + asString(dockerPlatformData?.hostname) || + getPreferredResourceHostname(resource) || + getPreferredResourceDisplayName(resource) || + resource.platformId || + resource.id; + const workloadAgentId = + proxmoxNodeName || + actionableDockerHostId || + kubernetesAgentId || + actionableAgentId || + asString(resource.parentName) || + resource.parentId || + getPreferredResourceHostname(resource) || + resource.platformId || + resource.id; + const hostname = getPreferredHostLabel(resource); + + switch (resource.type) { + case 'agent': + case 'docker-host': + case 'pbs': + case 'pmg': + case 'k8s-cluster': + case 'k8s-node': + case 'truenas': + return { + resourceType: 'agent', + agentId: agentLookupId, + resourceId: agentLookupId, + hostname, + metadataKind: 'agent', + metadataId: agentLookupId, + targetLabel: 'agent', + }; + case 'vm': + return { + resourceType: 'vm', + agentId: workloadAgentId, + resourceId: vmidResourceId || resource.id, + hostname, + metadataKind: 'guest', + metadataId: resource.id, + targetLabel: 'guest', + }; + case 'system-container': + case 'oci-container': + return { + resourceType: 'system-container', + agentId: workloadAgentId, + resourceId: vmidResourceId || resource.id, + hostname, + metadataKind: 'guest', + metadataId: resource.id, + targetLabel: 'guest', + }; + case 'app-container': + return { + resourceType: 'app-container', + agentId: workloadAgentId, + resourceId: asString(dockerPlatformData?.containerId) || resource.id, + hostname, + metadataKind: 'guest', + metadataId: resource.id, + targetLabel: 'container', + }; + case 'pod': + case 'k8s-deployment': + case 'k8s-service': + return { + resourceType: 'pod', + agentId: workloadAgentId, + resourceId: kubernetesResourceId || resource.id, + hostname, + metadataKind: 'guest', + metadataId: resource.id, + targetLabel: 'workload', + }; + default: + return null; + } +}; diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts b/frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts index 3f6eb61fd..b363acd24 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts @@ -9,21 +9,8 @@ import type { } from '@/types/api'; import type { Resource, ResourceMetric } from '@/types/resource'; import { formatTemperature } from '@/utils/temperature'; -import type { ResourceType as DiscoveryResourceType } from '@/types/discovery'; -import { - canonicalDiscoveryResourceType, - isAgentDiscoveryResourceType, -} from '@/utils/discoveryTarget'; -import { - getActionableAgentIdFromResource, - getActionableDockerRuntimeIdFromResource, - getActionableKubernetesClusterIdFromResource, -} from '@/utils/agentResources'; -import { - getPreferredResourceDisplayName, - getPreferredResourceClusterName, - getPreferredResourceHostname, -} from '@/utils/resourceIdentity'; +import { getActionableAgentIdFromResource } from '@/utils/agentResources'; +import { getPreferredResourceDisplayName, getPreferredResourceHostname } from '@/utils/resourceIdentity'; import { titleCaseDelimitedLabel } from '@/utils/textPresentation'; export { getSourceTypeLabel as formatSourceType } from '@/utils/sourceTypePresentation'; @@ -174,200 +161,14 @@ export type DockerPlatformData = { command?: DockerRuntimeCommand; }; -export type DiscoveryConfig = { - resourceType: DiscoveryResourceType; - agentId: string; - resourceId: string; - hostname: string; - metadataKind: 'guest' | 'agent'; - metadataId: string; - targetLabel: string; -}; - const asString = (value: unknown): string | undefined => typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; -const asNumber = (value: unknown): number | undefined => - typeof value === 'number' && Number.isFinite(value) ? value : undefined; - const getPreferredHostLabel = (resource: Resource): string => getPreferredResourceHostname(resource) || getPreferredResourceDisplayName(resource) || resource.id; -export const toDiscoveryConfig = (resource: Resource): DiscoveryConfig | null => { - const explicitDiscoveryTarget = resource.discoveryTarget; - const explicitDiscoveryAgentId = asString( - (explicitDiscoveryTarget as { agentId?: unknown } | undefined)?.agentId, - ); - - if ( - explicitDiscoveryTarget && - explicitDiscoveryTarget.resourceType && - explicitDiscoveryAgentId && - explicitDiscoveryTarget.resourceId - ) { - const explicitResourceType = canonicalDiscoveryResourceType( - explicitDiscoveryTarget.resourceType, - ); - const resourceType = (() => { - switch (explicitResourceType) { - case 'agent': - return 'agent'; - case 'vm': - case 'system-container': - case 'app-container': - case 'pod': - return explicitResourceType; - default: - return null; - } - })(); - - if (resourceType) { - const hostname = explicitDiscoveryTarget.hostname || getPreferredHostLabel(resource); - const isHostDiscovery = isAgentDiscoveryResourceType(resourceType); - const targetLabel = isHostDiscovery - ? 'agent' - : resourceType === 'app-container' - ? 'container' - : resourceType === 'pod' - ? 'workload' - : 'guest'; - return { - resourceType, - agentId: explicitDiscoveryAgentId, - resourceId: explicitDiscoveryTarget.resourceId, - hostname, - metadataKind: isHostDiscovery ? 'agent' : 'guest', - metadataId: explicitDiscoveryTarget.resourceId, - targetLabel, - }; - } - } - - const platformData = resource.platformData as PlatformData | undefined; - const dockerPlatformData = platformData?.docker as DockerPlatformData | undefined; - const kubernetesPlatformData = platformData?.kubernetes; - const proxmoxVmid = - asNumber(resource.proxmox?.vmid) ?? - asNumber(platformData?.proxmox?.vmid) ?? - asNumber((platformData as { vmid?: unknown } | undefined)?.vmid); - const vmidResourceId = - proxmoxVmid !== undefined && proxmoxVmid > 0 ? String(proxmoxVmid) : undefined; - const proxmoxNodeName = - asString(resource.proxmox?.nodeName) || - platformData?.proxmox?.nodeName || - asString((platformData as { nodeName?: unknown } | undefined)?.nodeName); - const actionableAgentId = getActionableAgentIdFromResource(resource); - const actionableDockerHostId = getActionableDockerRuntimeIdFromResource(resource); - const actionableKubernetesId = getActionableKubernetesClusterIdFromResource(resource); - const kubernetesAgentId = - asString(resource.kubernetes?.agentId) || - asString(kubernetesPlatformData?.agentId) || - actionableKubernetesId || - getPreferredResourceClusterName(resource); - const kubernetesResourceId = - asString(resource.kubernetes?.podUid) || - asString(kubernetesPlatformData?.podUid) || - (() => { - const namespace = - asString(resource.kubernetes?.namespace) || asString(kubernetesPlatformData?.namespace); - const podName = - asString(resource.kubernetes?.podName) || - asString(kubernetesPlatformData?.podName) || - asString(resource.name); - return namespace && podName ? `${namespace}/${podName}` : undefined; - })(); - const agentLookupId = - actionableDockerHostId || - actionableKubernetesId || - actionableAgentId || - proxmoxNodeName || - platformData?.agent?.hostname || - asString(dockerPlatformData?.hostname) || - getPreferredResourceHostname(resource) || - getPreferredResourceDisplayName(resource) || - resource.platformId || - resource.id; - const agentLikeId = agentLookupId; - const workloadAgentId = - proxmoxNodeName || - actionableDockerHostId || - kubernetesAgentId || - actionableAgentId || - asString(resource.parentName) || - resource.parentId || - getPreferredResourceHostname(resource) || - resource.platformId || - resource.id; - const hostname = getPreferredHostLabel(resource); - - switch (resource.type) { - case 'agent': - case 'docker-host': - case 'pbs': - case 'pmg': - case 'k8s-cluster': - case 'k8s-node': - case 'truenas': - return { - resourceType: 'agent', - agentId: agentLikeId, - resourceId: agentLikeId, - hostname, - metadataKind: 'agent', - metadataId: agentLikeId, - targetLabel: 'agent', - }; - case 'vm': - return { - resourceType: 'vm', - agentId: workloadAgentId, - resourceId: vmidResourceId || resource.id, - hostname, - metadataKind: 'guest', - metadataId: resource.id, - targetLabel: 'guest', - }; - case 'system-container': - case 'oci-container': - return { - resourceType: 'system-container', - agentId: workloadAgentId, - resourceId: vmidResourceId || resource.id, - hostname, - metadataKind: 'guest', - metadataId: resource.id, - targetLabel: 'guest', - }; - case 'app-container': - return { - resourceType: 'app-container', - agentId: workloadAgentId, - resourceId: asString(dockerPlatformData?.containerId) || resource.id, - hostname, - metadataKind: 'guest', - metadataId: resource.id, - targetLabel: 'container', - }; - case 'pod': - case 'k8s-deployment': - case 'k8s-service': - return { - resourceType: 'pod', - agentId: workloadAgentId, - resourceId: kubernetesResourceId || resource.id, - hostname, - metadataKind: 'guest', - metadataId: resource.id, - targetLabel: 'workload', - }; - default: - return null; - } -}; - export const buildMemory = (metric?: ResourceMetric, fallback?: Partial): Memory => { const total = metric?.total ?? fallback?.total ?? 0; const used = metric?.used ?? fallback?.used ?? 0; diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts index ee83e9cba..814acf182 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts @@ -31,13 +31,13 @@ import { ALIAS_COLLAPSE_THRESHOLD, buildTemperatureRows, toAgentFromResource, - toDiscoveryConfig, toNodeFromProxmox, type AgentPlatformData, type DockerPlatformData, type KubernetesPlatformData, type PlatformData, } from '@/components/Infrastructure/resourceDetailMappers'; +import { toDiscoveryConfig } from '@/components/Infrastructure/resourceDetailDiscoveryModel'; import { formatIdentifierLabel } from '@/utils/textPresentation'; import { buildPbsVisibleJobBreakdown, diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 723b20f1a..ee14b8950 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -337,6 +337,7 @@ import resourceDetailDrawerDebugSource from '@/components/Infrastructure/Resourc import infrastructureSummarySource from '@/components/Infrastructure/InfrastructureSummary.tsx?raw'; import infrastructureSummaryStateSource from '@/components/Infrastructure/useInfrastructureSummaryState.ts?raw'; import infrastructureSummaryModelSource from '@/components/Infrastructure/infrastructureSummaryModel.ts?raw'; +import resourceDetailDrawerDiscoveryModelSource from '@/components/Infrastructure/resourceDetailDiscoveryModel.ts?raw'; import resourceDetailMappersSource from '@/components/Infrastructure/resourceDetailMappers.ts?raw'; import resourceDetailDrawerHistoryStateSource from '@/components/Infrastructure/useResourceDetailDrawerHistoryState.ts?raw'; import resourceDetailDrawerDerivedStateSource from '@/components/Infrastructure/useResourceDetailDrawerDerivedState.ts?raw'; @@ -988,10 +989,15 @@ describe('frontend resource type boundaries', () => { expect(resourceDetailDrawerStateSource).not.toContain('createResource('); expect(resourceDetailDrawerStateSource).not.toContain('MonitoringAPI.'); expect(resourceDetailDrawerHistoryStateSource).toContain('createResource('); - expect(resourceDetailDrawerDerivedStateSource).toContain('buildRelatedLinks'); + expect(resourceDetailDrawerDerivedStateSource).toContain( + "from '@/components/Infrastructure/resourceDetailDiscoveryModel'", + ); + expect(resourceDetailDrawerDerivedStateSource).toContain('buildWorkloadsHref'); + expect(resourceDetailDrawerDerivedStateSource).toContain('buildServiceDetailLinks'); expect(resourceDetailDrawerDerivedStateSource).toContain( "from './resourceDetailDrawerServiceModel'", ); + expect(resourceDetailDrawerDiscoveryModelSource).toContain('export const toDiscoveryConfig'); expect(resourceDetailDrawerServiceModelSource).toContain('getServiceDetailsSummary'); expect(resourceDetailDrawerOverviewSource).not.toContain('MonitoringAPI.'); expect(resourceDetailDrawerOverviewSource).toContain('drawer.queueDockerUpdateCheck'); @@ -3142,6 +3148,7 @@ describe('frontend resource type boundaries', () => { expect(useUnifiedResourcesSource).not.toContain('const resolvePolicyRedactionHints ='); expect(useUnifiedResourcesSource).not.toContain('const resolvePolicy ='); expect(resourceDetailMappersSource).toContain('titleCaseDelimitedLabel'); + expect(resourceDetailMappersSource).not.toContain('export const toDiscoveryConfig'); expect(resourceDetailMappersSource).not.toContain('export const normalizeHealthLabel'); expect(resourceDetailMappersSource).not.toContain('export const healthToneClass'); expect(unifiedResourceTableSource).toContain('useUnifiedResourceTableState'); diff --git a/scripts/release_control/canonical_completion_guard_test.py b/scripts/release_control/canonical_completion_guard_test.py index 2c0fbcac4..7aa5d16ef 100644 --- a/scripts/release_control/canonical_completion_guard_test.py +++ b/scripts/release_control/canonical_completion_guard_test.py @@ -499,6 +499,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase): "allow_same_subsystem_tests": False, "test_prefixes": [], "exact_files": [ + "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts", "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx", "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx", "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx", diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index 0e79dc75e..3910232c5 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -156,6 +156,36 @@ class SubsystemLookupTest(unittest.TestCase): match["verification_requirement"]["exact_files"], ) + def test_lookup_paths_assigns_resource_detail_drawer_discovery_model_to_unified_resources(self) -> None: + result = lookup_paths( + ["frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts"] + ) + self.assertEqual(result["unowned_runtime_files"], []) + self.assertEqual( + {item["subsystem"] for item in result["impacted_subsystems"]}, + {"unified-resources"}, + ) + file_entry = result["files"][0] + self.assertEqual(file_entry["classification"], "runtime") + self.assertEqual( + {match["subsystem"] for match in file_entry["matches"]}, + {"unified-resources"}, + ) + match = file_entry["matches"][0] + self.assertEqual( + match["contract"], + "docs/release-control/v6/internal/subsystems/unified-resources.md", + ) + self.assertEqual(match["lane_context"]["lane_id"], "L13") + self.assertEqual( + match["verification_requirement"]["id"], + "resource-consumers", + ) + self.assertIn( + "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts", + match["verification_requirement"]["exact_files"], + ) + def test_lookup_paths_assigns_recent_alerts_panel_to_alerts(self) -> None: result = lookup_paths(["frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx"]) self.assertEqual(result["unowned_runtime_files"], []) @@ -3994,6 +4024,7 @@ class SubsystemLookupTest(unittest.TestCase): self.assertEqual( match["verification_requirement"]["exact_files"], [ + "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.discovery.test.ts", "frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx", "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx", "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx",