Skip to content

Commit 71764a7

Browse files
fix: Resources on graph based on name AND apiVersion (#307)
Co-authored-by: Andreas Kienle <andreas.kienle@sap.com>
1 parent 0662013 commit 71764a7

File tree

3 files changed

+189
-83
lines changed

3 files changed

+189
-83
lines changed

src/components/Graphs/graphUtils.spec.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { describe, it, expect } from 'vitest';
2-
import { getStatusCondition, resolveProviderType, generateColorMap } from './graphUtils';
3-
import { ProviderConfigs } from '../../lib/shared/types';
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { getStatusCondition, resolveProviderType, generateColorMap, buildTreeData } from './graphUtils';
3+
import { ProviderConfigs, ManagedResourceGroup, ManagedResourceItem } from '../../lib/shared/types';
44

55
describe('getStatusCondition', () => {
66
it('returns the Ready condition when present', () => {
@@ -89,3 +89,91 @@ describe('generateColorMap', () => {
8989
expect(generateColorMap([], 'provider')).toEqual({});
9090
});
9191
});
92+
93+
describe('buildTreeData', () => {
94+
const mockOnYamlClick = vi.fn();
95+
const mockProviderConfigsList: ProviderConfigs[] = [
96+
{
97+
provider: 'test-provider',
98+
items: [{ metadata: { name: 'test-config' }, apiVersion: 'btp/v1' }],
99+
},
100+
] as any;
101+
102+
it('builds tree data for single item', () => {
103+
const item: ManagedResourceItem = {
104+
metadata: { name: 'test-resource' },
105+
apiVersion: 'v1',
106+
kind: 'TestKind',
107+
spec: {
108+
providerConfigRef: { name: 'test-config' },
109+
forProvider: {},
110+
},
111+
status: { conditions: [{ type: 'Ready', status: 'True', lastTransitionTime: '2024-01-01' }] },
112+
} as any;
113+
114+
const managedResources: ManagedResourceGroup[] = [{ items: [item] }];
115+
const result = buildTreeData(managedResources, mockProviderConfigsList, mockOnYamlClick);
116+
117+
expect(result).toHaveLength(1);
118+
expect(result[0]).toMatchObject({
119+
id: 'test-resource-v1',
120+
label: 'test-resource-v1',
121+
type: 'TestKind',
122+
providerConfigName: 'test-config',
123+
status: 'OK',
124+
parentId: undefined,
125+
extraRefs: [],
126+
});
127+
});
128+
129+
it('builds tree data with references', () => {
130+
const item: ManagedResourceItem = {
131+
metadata: { name: 'space-resource' },
132+
apiVersion: 'v1beta1',
133+
kind: 'Space',
134+
spec: {
135+
providerConfigRef: { name: 'cf-config' },
136+
forProvider: {
137+
subaccountRef: { name: 'my-subaccount' },
138+
orgRef: { name: 'my-org' },
139+
},
140+
},
141+
status: { conditions: [{ type: 'Ready', status: 'False' }] },
142+
} as any;
143+
144+
const managedResources: ManagedResourceGroup[] = [{ items: [item] }];
145+
const result = buildTreeData(managedResources, mockProviderConfigsList, mockOnYamlClick);
146+
147+
expect(result[0]).toMatchObject({
148+
id: 'space-resource-v1beta1',
149+
parentId: 'my-subaccount-v1beta1',
150+
extraRefs: ['my-org-v1beta1'],
151+
status: 'ERROR',
152+
});
153+
});
154+
155+
it('creates separate nodes for items with same name but different apiVersion', () => {
156+
const item1: ManagedResourceItem = {
157+
metadata: { name: 'same-resource' },
158+
apiVersion: 'v1',
159+
kind: 'TestKind',
160+
spec: { providerConfigRef: { name: 'test-config' }, forProvider: {} },
161+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
162+
} as any;
163+
164+
const item2: ManagedResourceItem = {
165+
metadata: { name: 'same-resource' },
166+
apiVersion: 'v1beta1',
167+
kind: 'TestKind',
168+
spec: { providerConfigRef: { name: 'test-config' }, forProvider: {} },
169+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
170+
} as any;
171+
172+
const managedResources: ManagedResourceGroup[] = [{ items: [item1, item2] }];
173+
const result = buildTreeData(managedResources, mockProviderConfigsList, mockOnYamlClick);
174+
175+
expect(result).toHaveLength(2);
176+
expect(result.map((r) => r.id)).toEqual(['same-resource-v1', 'same-resource-v1beta1']);
177+
expect(result.map((r) => r.label)).toEqual(['same-resource-v1', 'same-resource-v1beta1']);
178+
});
179+
});

src/components/Graphs/graphUtils.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Condition, ManagedResourceItem, ProviderConfigs } from '../../lib/shared/types';
1+
import { Condition, ManagedResourceItem, ProviderConfigs, ManagedResourceGroup } from '../../lib/shared/types';
22
import { NodeData } from './types';
33

44
export type StatusType = 'ERROR' | 'OK';
@@ -81,3 +81,94 @@ export function extractRefs(item: ManagedResourceItem) {
8181
globalaccountTrustConfigurationRef: item?.spec?.forProvider?.globalaccountTrustConfigurationRef?.name,
8282
};
8383
}
84+
85+
export function buildTreeData(
86+
managedResources: ManagedResourceGroup[] | undefined,
87+
providerConfigsList: ProviderConfigs[],
88+
onYamlClick: (item: ManagedResourceItem) => void,
89+
): NodeData[] {
90+
if (!managedResources || !providerConfigsList) return [];
91+
92+
const allNodesMap = new Map<string, NodeData>();
93+
94+
managedResources.forEach((group: ManagedResourceGroup) => {
95+
group.items?.forEach((item: ManagedResourceItem) => {
96+
const name = item?.metadata?.name;
97+
const apiVersion = item?.apiVersion ?? '';
98+
const id = `${name}-${apiVersion}`;
99+
const kind = item?.kind;
100+
const providerConfigName = item?.spec?.providerConfigRef?.name ?? 'unknown';
101+
const providerType = resolveProviderType(providerConfigName, providerConfigsList);
102+
const statusCond = getStatusCondition(item?.status?.conditions);
103+
const status = statusCond?.status === 'True' ? 'OK' : 'ERROR';
104+
105+
let fluxName: string | undefined;
106+
const labelsMap = (item.metadata as unknown as { labels?: Record<string, string> }).labels;
107+
if (labelsMap) {
108+
const key = Object.keys(labelsMap).find((k) => k.endsWith('/name'));
109+
if (key) fluxName = labelsMap[key];
110+
}
111+
112+
const {
113+
subaccountRef,
114+
serviceManagerRef,
115+
spaceRef,
116+
orgRef,
117+
cloudManagementRef,
118+
directoryRef,
119+
entitlementRef,
120+
globalAccountRef,
121+
orgRoleRef,
122+
spaceMembersRef,
123+
cloudFoundryEnvironmentRef,
124+
kymaEnvironmentRef,
125+
roleCollectionRef,
126+
roleCollectionAssignmentRef,
127+
subaccountTrustConfigurationRef,
128+
globalaccountTrustConfigurationRef,
129+
} = extractRefs(item);
130+
131+
const createReferenceIdWithApiVersion = (referenceName: string | undefined) => {
132+
if (!referenceName) return undefined;
133+
return `${referenceName}-${apiVersion}`;
134+
};
135+
136+
if (id) {
137+
allNodesMap.set(id, {
138+
id,
139+
label: id,
140+
type: kind,
141+
providerConfigName,
142+
providerType,
143+
status,
144+
transitionTime: statusCond?.lastTransitionTime ?? '',
145+
statusMessage: statusCond?.reason ?? statusCond?.message ?? '',
146+
fluxName,
147+
parentId: createReferenceIdWithApiVersion(serviceManagerRef || subaccountRef),
148+
extraRefs: [
149+
spaceRef,
150+
orgRef,
151+
cloudManagementRef,
152+
directoryRef,
153+
entitlementRef,
154+
globalAccountRef,
155+
orgRoleRef,
156+
spaceMembersRef,
157+
cloudFoundryEnvironmentRef,
158+
kymaEnvironmentRef,
159+
roleCollectionRef,
160+
roleCollectionAssignmentRef,
161+
subaccountTrustConfigurationRef,
162+
globalaccountTrustConfigurationRef,
163+
]
164+
.map(createReferenceIdWithApiVersion)
165+
.filter(Boolean) as string[],
166+
item,
167+
onYamlClick,
168+
});
169+
}
170+
});
171+
});
172+
173+
return Array.from(allNodesMap.values());
174+
}

src/components/Graphs/useGraph.ts

Lines changed: 6 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { resourcesInterval } from '../../lib/shared/constants';
55
import { Node, Edge, Position, MarkerType } from '@xyflow/react';
66
import dagre from 'dagre';
77
import { NodeData, ColorBy } from './types';
8-
import { extractRefs, generateColorMap, getStatusCondition, resolveProviderType } from './graphUtils';
9-
import { ManagedResourceGroup, ManagedResourceItem } from '../../lib/shared/types';
8+
import { buildTreeData, generateColorMap } from './graphUtils';
9+
import { ManagedResourceItem } from '../../lib/shared/types';
1010

1111
const nodeWidth = 250;
1212
const nodeHeight = 60;
@@ -97,83 +97,10 @@ export function useGraph(colorBy: ColorBy, onYamlClick: (item: ManagedResourceIt
9797
const loading = managedResourcesLoading || providerConfigsLoading;
9898
const error = managedResourcesError || providerConfigsError;
9999

100-
const treeData = useMemo(() => {
101-
if (!managedResources || !providerConfigsList) return [];
102-
const allNodesMap = new Map<string, NodeData>();
103-
managedResources.forEach((group: ManagedResourceGroup) => {
104-
group.items?.forEach((item: ManagedResourceItem) => {
105-
const id = item?.metadata?.name;
106-
const kind = item?.kind;
107-
const providerConfigName = item?.spec?.providerConfigRef?.name ?? 'unknown';
108-
const providerType = resolveProviderType(providerConfigName, providerConfigsList);
109-
const statusCond = getStatusCondition(item?.status?.conditions);
110-
const status = statusCond?.status === 'True' ? 'OK' : 'ERROR';
111-
112-
let fluxName: string | undefined;
113-
const labelsMap = (item.metadata as unknown as { labels?: Record<string, string> }).labels;
114-
if (labelsMap) {
115-
const key = Object.keys(labelsMap).find((k) => k.endsWith('/name'));
116-
if (key) fluxName = labelsMap[key];
117-
}
118-
119-
const {
120-
subaccountRef,
121-
serviceManagerRef,
122-
spaceRef,
123-
orgRef,
124-
cloudManagementRef,
125-
directoryRef,
126-
entitlementRef,
127-
globalAccountRef,
128-
orgRoleRef,
129-
spaceMembersRef,
130-
cloudFoundryEnvironmentRef,
131-
kymaEnvironmentRef,
132-
roleCollectionRef,
133-
roleCollectionAssignmentRef,
134-
subaccountTrustConfigurationRef,
135-
globalaccountTrustConfigurationRef,
136-
} = extractRefs(item);
137-
138-
const parentId = serviceManagerRef || subaccountRef;
139-
const extraRefs = [
140-
spaceRef,
141-
orgRef,
142-
cloudManagementRef,
143-
directoryRef,
144-
entitlementRef,
145-
globalAccountRef,
146-
orgRoleRef,
147-
spaceMembersRef,
148-
cloudFoundryEnvironmentRef,
149-
kymaEnvironmentRef,
150-
roleCollectionRef,
151-
roleCollectionAssignmentRef,
152-
subaccountTrustConfigurationRef,
153-
globalaccountTrustConfigurationRef,
154-
].filter(Boolean) as string[];
155-
156-
if (id) {
157-
allNodesMap.set(id, {
158-
id,
159-
label: id,
160-
type: kind,
161-
providerConfigName,
162-
providerType,
163-
status,
164-
transitionTime: statusCond?.lastTransitionTime ?? '',
165-
statusMessage: statusCond?.reason ?? statusCond?.message ?? '',
166-
fluxName,
167-
parentId,
168-
extraRefs,
169-
item,
170-
onYamlClick,
171-
});
172-
}
173-
});
174-
});
175-
return Array.from(allNodesMap.values());
176-
}, [managedResources, providerConfigsList, onYamlClick]);
100+
const treeData = useMemo(
101+
() => buildTreeData(managedResources, providerConfigsList, onYamlClick),
102+
[managedResources, providerConfigsList, onYamlClick],
103+
);
177104

178105
const colorMap = useMemo(() => generateColorMap(treeData, colorBy), [treeData, colorBy]);
179106

0 commit comments

Comments
 (0)