Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 91 additions & 3 deletions src/components/Graphs/graphUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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']);
});
});
93 changes: 92 additions & 1 deletion src/components/Graphs/graphUtils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<string, NodeData>();

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<string, string> }).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());
}
85 changes: 6 additions & 79 deletions src/components/Graphs/useGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, NodeData>();
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<string, string> }).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]);

Expand Down
Loading