diff --git a/src/components/Graphs/graphUtils.spec.ts b/src/components/Graphs/graphUtils.spec.ts index f4340194..15831bac 100644 --- a/src/components/Graphs/graphUtils.spec.ts +++ b/src/components/Graphs/graphUtils.spec.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from 'vitest'; -import { getStatusCondition, resolveProviderType, generateColorMap } from './graphUtils'; -import { ProviderConfigs } from '../../lib/shared/types'; +import { describe, it, expect, vi } from 'vitest'; +import { getStatusCondition, resolveProviderType, generateColorMap, buildTreeData } from './graphUtils'; +import { ProviderConfigs, ManagedResourceGroup, ManagedResourceItem } from '../../lib/shared/types'; describe('getStatusCondition', () => { it('returns the Ready condition when present', () => { @@ -89,3 +89,91 @@ describe('generateColorMap', () => { expect(generateColorMap([], 'provider')).toEqual({}); }); }); + +describe('buildTreeData', () => { + const mockOnYamlClick = vi.fn(); + const mockProviderConfigsList: ProviderConfigs[] = [ + { + provider: 'test-provider', + items: [{ metadata: { name: 'test-config' }, apiVersion: 'btp/v1' }], + }, + ] as any; + + it('builds tree data for single item', () => { + const item: ManagedResourceItem = { + metadata: { name: 'test-resource' }, + apiVersion: 'v1', + kind: 'TestKind', + spec: { + providerConfigRef: { name: 'test-config' }, + forProvider: {}, + }, + status: { conditions: [{ type: 'Ready', status: 'True', lastTransitionTime: '2024-01-01' }] }, + } as any; + + const managedResources: ManagedResourceGroup[] = [{ items: [item] }]; + const result = buildTreeData(managedResources, mockProviderConfigsList, mockOnYamlClick); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + id: 'test-resource-v1', + label: 'test-resource-v1', + type: 'TestKind', + providerConfigName: 'test-config', + status: 'OK', + parentId: undefined, + extraRefs: [], + }); + }); + + it('builds tree data with references', () => { + const item: ManagedResourceItem = { + metadata: { name: 'space-resource' }, + apiVersion: 'v1beta1', + kind: 'Space', + spec: { + providerConfigRef: { name: 'cf-config' }, + forProvider: { + subaccountRef: { name: 'my-subaccount' }, + orgRef: { name: 'my-org' }, + }, + }, + status: { conditions: [{ type: 'Ready', status: 'False' }] }, + } as any; + + const managedResources: ManagedResourceGroup[] = [{ items: [item] }]; + const result = buildTreeData(managedResources, mockProviderConfigsList, mockOnYamlClick); + + expect(result[0]).toMatchObject({ + id: 'space-resource-v1beta1', + parentId: 'my-subaccount-v1beta1', + extraRefs: ['my-org-v1beta1'], + status: 'ERROR', + }); + }); + + it('creates separate nodes for items with same name but different apiVersion', () => { + const item1: ManagedResourceItem = { + metadata: { name: 'same-resource' }, + apiVersion: 'v1', + kind: 'TestKind', + spec: { providerConfigRef: { name: 'test-config' }, forProvider: {} }, + status: { conditions: [{ type: 'Ready', status: 'True' }] }, + } as any; + + const item2: ManagedResourceItem = { + metadata: { name: 'same-resource' }, + apiVersion: 'v1beta1', + kind: 'TestKind', + spec: { providerConfigRef: { name: 'test-config' }, forProvider: {} }, + status: { conditions: [{ type: 'Ready', status: 'True' }] }, + } as any; + + const managedResources: ManagedResourceGroup[] = [{ items: [item1, item2] }]; + const result = buildTreeData(managedResources, mockProviderConfigsList, mockOnYamlClick); + + expect(result).toHaveLength(2); + expect(result.map((r) => r.id)).toEqual(['same-resource-v1', 'same-resource-v1beta1']); + expect(result.map((r) => r.label)).toEqual(['same-resource-v1', 'same-resource-v1beta1']); + }); +}); diff --git a/src/components/Graphs/graphUtils.ts b/src/components/Graphs/graphUtils.ts index 9dc51d02..390eed08 100644 --- a/src/components/Graphs/graphUtils.ts +++ b/src/components/Graphs/graphUtils.ts @@ -1,4 +1,4 @@ -import { Condition, ManagedResourceItem, ProviderConfigs } from '../../lib/shared/types'; +import { Condition, ManagedResourceItem, ProviderConfigs, ManagedResourceGroup } from '../../lib/shared/types'; import { NodeData } from './types'; export type StatusType = 'ERROR' | 'OK'; @@ -81,3 +81,94 @@ export function extractRefs(item: ManagedResourceItem) { globalaccountTrustConfigurationRef: item?.spec?.forProvider?.globalaccountTrustConfigurationRef?.name, }; } + +export function buildTreeData( + managedResources: ManagedResourceGroup[] | undefined, + providerConfigsList: ProviderConfigs[], + onYamlClick: (item: ManagedResourceItem) => void, +): NodeData[] { + if (!managedResources || !providerConfigsList) return []; + + const allNodesMap = new Map(); + + managedResources.forEach((group: ManagedResourceGroup) => { + group.items?.forEach((item: ManagedResourceItem) => { + const name = item?.metadata?.name; + const apiVersion = item?.apiVersion ?? ''; + const id = `${name}-${apiVersion}`; + const kind = item?.kind; + const providerConfigName = item?.spec?.providerConfigRef?.name ?? 'unknown'; + const providerType = resolveProviderType(providerConfigName, providerConfigsList); + const statusCond = getStatusCondition(item?.status?.conditions); + const status = statusCond?.status === 'True' ? 'OK' : 'ERROR'; + + let fluxName: string | undefined; + const labelsMap = (item.metadata as unknown as { labels?: Record }).labels; + if (labelsMap) { + const key = Object.keys(labelsMap).find((k) => k.endsWith('/name')); + if (key) fluxName = labelsMap[key]; + } + + const { + subaccountRef, + serviceManagerRef, + spaceRef, + orgRef, + cloudManagementRef, + directoryRef, + entitlementRef, + globalAccountRef, + orgRoleRef, + spaceMembersRef, + cloudFoundryEnvironmentRef, + kymaEnvironmentRef, + roleCollectionRef, + roleCollectionAssignmentRef, + subaccountTrustConfigurationRef, + globalaccountTrustConfigurationRef, + } = extractRefs(item); + + const createReferenceIdWithApiVersion = (referenceName: string | undefined) => { + if (!referenceName) return undefined; + return `${referenceName}-${apiVersion}`; + }; + + if (id) { + allNodesMap.set(id, { + id, + label: id, + type: kind, + providerConfigName, + providerType, + status, + transitionTime: statusCond?.lastTransitionTime ?? '', + statusMessage: statusCond?.reason ?? statusCond?.message ?? '', + fluxName, + parentId: createReferenceIdWithApiVersion(serviceManagerRef || subaccountRef), + extraRefs: [ + spaceRef, + orgRef, + cloudManagementRef, + directoryRef, + entitlementRef, + globalAccountRef, + orgRoleRef, + spaceMembersRef, + cloudFoundryEnvironmentRef, + kymaEnvironmentRef, + roleCollectionRef, + roleCollectionAssignmentRef, + subaccountTrustConfigurationRef, + globalaccountTrustConfigurationRef, + ] + .map(createReferenceIdWithApiVersion) + .filter(Boolean) as string[], + item, + onYamlClick, + }); + } + }); + }); + + return Array.from(allNodesMap.values()); +} diff --git a/src/components/Graphs/useGraph.ts b/src/components/Graphs/useGraph.ts index 92e72db6..d8c53531 100644 --- a/src/components/Graphs/useGraph.ts +++ b/src/components/Graphs/useGraph.ts @@ -5,8 +5,8 @@ import { resourcesInterval } from '../../lib/shared/constants'; import { Node, Edge, Position, MarkerType } from '@xyflow/react'; import dagre from 'dagre'; import { NodeData, ColorBy } from './types'; -import { extractRefs, generateColorMap, getStatusCondition, resolveProviderType } from './graphUtils'; -import { ManagedResourceGroup, ManagedResourceItem } from '../../lib/shared/types'; +import { buildTreeData, generateColorMap } from './graphUtils'; +import { ManagedResourceItem } from '../../lib/shared/types'; const nodeWidth = 250; const nodeHeight = 60; @@ -97,83 +97,10 @@ export function useGraph(colorBy: ColorBy, onYamlClick: (item: ManagedResourceIt const loading = managedResourcesLoading || providerConfigsLoading; const error = managedResourcesError || providerConfigsError; - const treeData = useMemo(() => { - if (!managedResources || !providerConfigsList) return []; - const allNodesMap = new Map(); - managedResources.forEach((group: ManagedResourceGroup) => { - group.items?.forEach((item: ManagedResourceItem) => { - const id = item?.metadata?.name; - const kind = item?.kind; - const providerConfigName = item?.spec?.providerConfigRef?.name ?? 'unknown'; - const providerType = resolveProviderType(providerConfigName, providerConfigsList); - const statusCond = getStatusCondition(item?.status?.conditions); - const status = statusCond?.status === 'True' ? 'OK' : 'ERROR'; - - let fluxName: string | undefined; - const labelsMap = (item.metadata as unknown as { labels?: Record }).labels; - if (labelsMap) { - const key = Object.keys(labelsMap).find((k) => k.endsWith('/name')); - if (key) fluxName = labelsMap[key]; - } - - const { - subaccountRef, - serviceManagerRef, - spaceRef, - orgRef, - cloudManagementRef, - directoryRef, - entitlementRef, - globalAccountRef, - orgRoleRef, - spaceMembersRef, - cloudFoundryEnvironmentRef, - kymaEnvironmentRef, - roleCollectionRef, - roleCollectionAssignmentRef, - subaccountTrustConfigurationRef, - globalaccountTrustConfigurationRef, - } = extractRefs(item); - - const parentId = serviceManagerRef || subaccountRef; - const extraRefs = [ - spaceRef, - orgRef, - cloudManagementRef, - directoryRef, - entitlementRef, - globalAccountRef, - orgRoleRef, - spaceMembersRef, - cloudFoundryEnvironmentRef, - kymaEnvironmentRef, - roleCollectionRef, - roleCollectionAssignmentRef, - subaccountTrustConfigurationRef, - globalaccountTrustConfigurationRef, - ].filter(Boolean) as string[]; - - if (id) { - allNodesMap.set(id, { - id, - label: id, - type: kind, - providerConfigName, - providerType, - status, - transitionTime: statusCond?.lastTransitionTime ?? '', - statusMessage: statusCond?.reason ?? statusCond?.message ?? '', - fluxName, - parentId, - extraRefs, - item, - onYamlClick, - }); - } - }); - }); - return Array.from(allNodesMap.values()); - }, [managedResources, providerConfigsList, onYamlClick]); + const treeData = useMemo( + () => buildTreeData(managedResources, providerConfigsList, onYamlClick), + [managedResources, providerConfigsList, onYamlClick], + ); const colorMap = useMemo(() => generateColorMap(treeData, colorBy), [treeData, colorBy]);