Skip to content

Commit b2a7e29

Browse files
lheinshivamG640
authored andcommitted
feat: Topology view
1 parent a44446a commit b2a7e29

37 files changed

Lines changed: 975 additions & 206 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,4 @@ storybook-static
149149
.bob/notes
150150
.bob/skills
151151
.codex
152+
.claude/

packages/ui-tests/stories/canvas/Canvas.stories.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BaseVisualEntity,
3+
buildDesignerCanvasModel,
34
CamelRouteVisualEntity,
45
Canvas,
56
CatalogLoaderProvider,
@@ -65,8 +66,18 @@ const CanvasFromEntity: StoryFn<CanvasStoryArgs> = ({ entity }) => {
6566
const entities = useMemo(() => [entity], [entity]);
6667
const visibleFlows = useMemo(() => ({ [entity.id]: true }), [entity.id]);
6768
const { vizNodes, isResolving } = useVisibleVizNodes(entities, visibleFlows);
69+
const { nodes, edges } = useMemo(() => buildDesignerCanvasModel(vizNodes), [vizNodes]);
6870

69-
return <Canvas vizNodes={vizNodes} entitiesCount={1} isVizNodesResolving={isResolving} />;
71+
return (
72+
<Canvas
73+
nodes={nodes}
74+
edges={edges}
75+
entitiesCount={1}
76+
visibleEntitiesCount={vizNodes.length}
77+
isModelResolving={isResolving}
78+
applyCollapseOnUpdate
79+
/>
80+
);
7081
};
7182

7283
const ContextDecorator = (Story: StoryFn) => {

packages/ui/src/components/Visualization/Canvas/Canvas.test.tsx

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { VisibleFlowsContextResult } from '../../../providers/visible-flows.prov
1515
import { TestProvidersWrapper, TestRuntimeProviderWrapper } from '../../../stubs';
1616
import { camelRouteJson } from '../../../stubs/camel-route';
1717
import { kameletJson } from '../../../stubs/kamelet-route';
18+
import { buildDesignerCanvasModel } from '../designer-canvas-model';
1819
import { Canvas } from './Canvas';
1920
import { LayoutType } from './canvas.models';
2021
import { ControllerService } from './controller.service';
@@ -25,6 +26,17 @@ vi.mock('./apply-collapse-state', () => ({
2526

2627
import { applyCollapseState } from './apply-collapse-state';
2728

29+
function canvasPropsFromVizNodes(vizNodes: IVisualizationNode[], entitiesCount: number) {
30+
const { nodes, edges } = buildDesignerCanvasModel(vizNodes);
31+
return {
32+
nodes,
33+
edges,
34+
entitiesCount,
35+
visibleEntitiesCount: vizNodes.length,
36+
applyCollapseOnUpdate: true,
37+
};
38+
}
39+
2840
describe('Canvas', () => {
2941
const entity = new CamelRouteVisualEntity(camelRouteJson);
3042

@@ -47,7 +59,7 @@ describe('Canvas', () => {
4759
result = render(
4860
<Provider>
4961
<VisualizationProvider controller={ControllerService.createController()}>
50-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
62+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
5163
</VisualizationProvider>
5264
</Provider>,
5365
);
@@ -74,7 +86,7 @@ describe('Canvas', () => {
7486
render(
7587
<Provider>
7688
<VisualizationProvider controller={controller}>
77-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
89+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
7890
</VisualizationProvider>
7991
</Provider>,
8092
);
@@ -114,7 +126,7 @@ describe('Canvas', () => {
114126
setVizNodesState = setVizNodes;
115127
return (
116128
<VisualizationProvider controller={controller}>
117-
<Canvas vizNodes={vizNodes} entitiesCount={1} />
129+
<Canvas {...canvasPropsFromVizNodes(vizNodes, 1)} />
118130
</VisualizationProvider>
119131
);
120132
};
@@ -160,7 +172,7 @@ describe('Canvas', () => {
160172
<ActionConfirmationModalContextProvider>
161173
<Provider>
162174
<VisualizationProvider controller={ControllerService.createController()}>
163-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
175+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
164176
</VisualizationProvider>
165177
</Provider>
166178
</ActionConfirmationModalContextProvider>,
@@ -220,7 +232,7 @@ describe('Canvas', () => {
220232
<ActionConfirmationModalContextProvider>
221233
<Provider>
222234
<VisualizationProvider controller={ControllerService.createController()}>
223-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
235+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
224236
</VisualizationProvider>
225237
</Provider>
226238
</ActionConfirmationModalContextProvider>,
@@ -273,7 +285,7 @@ describe('Canvas', () => {
273285
<CatalogModalContext.Provider value={{ getNewComponent: vi.fn(), checkCompatibility: vi.fn() }}>
274286
<Provider>
275287
<VisualizationProvider controller={ControllerService.createController()}>
276-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
288+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
277289
</VisualizationProvider>
278290
</Provider>
279291
</CatalogModalContext.Provider>,
@@ -300,7 +312,7 @@ describe('Canvas', () => {
300312
result = render(
301313
<Provider>
302314
<VisualizationProvider controller={ControllerService.createController()}>
303-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
315+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
304316
</VisualizationProvider>
305317
</Provider>,
306318
);
@@ -331,7 +343,7 @@ describe('Canvas', () => {
331343
<RuntimeProvider>
332344
<Provider>
333345
<VisualizationProvider controller={ControllerService.createController()}>
334-
<Canvas vizNodes={[]} entitiesCount={0} />
346+
<Canvas nodes={[]} edges={[]} entitiesCount={0} visibleEntitiesCount={0} />
335347
</VisualizationProvider>
336348
</Provider>
337349
</RuntimeProvider>,
@@ -358,7 +370,7 @@ describe('Canvas', () => {
358370
<RuntimeProvider>
359371
<Provider>
360372
<VisualizationProvider controller={ControllerService.createController()}>
361-
<Canvas vizNodes={[]} entitiesCount={1} />
373+
<Canvas nodes={[]} edges={[]} entitiesCount={1} visibleEntitiesCount={0} />
362374
</VisualizationProvider>
363375
</Provider>
364376
</RuntimeProvider>,
@@ -384,7 +396,7 @@ describe('Canvas', () => {
384396
<RuntimeProvider>
385397
<Provider>
386398
<VisualizationProvider controller={ControllerService.createController()}>
387-
<Canvas vizNodes={[]} entitiesCount={1} isVizNodesResolving />
399+
<Canvas nodes={[]} edges={[]} entitiesCount={1} visibleEntitiesCount={0} isModelResolving />
388400
</VisualizationProvider>
389401
</Provider>
390402
</RuntimeProvider>,
@@ -446,7 +458,7 @@ describe('Canvas', () => {
446458
<SettingsProvider adapter={settingsAdapter}>
447459
<Provider>
448460
<VisualizationProvider controller={controller}>
449-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
461+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
450462
</VisualizationProvider>
451463
</Provider>
452464
</SettingsProvider>,
@@ -489,7 +501,7 @@ describe('Canvas', () => {
489501
<SettingsProvider adapter={settingsAdapter}>
490502
<Provider>
491503
<VisualizationProvider controller={ControllerService.createController()}>
492-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
504+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
493505
</VisualizationProvider>
494506
</Provider>
495507
</SettingsProvider>,
@@ -531,7 +543,7 @@ describe('Canvas', () => {
531543
<SettingsProvider adapter={settingsAdapter}>
532544
<Provider>
533545
<VisualizationProvider controller={ControllerService.createController()}>
534-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
546+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
535547
</VisualizationProvider>
536548
</Provider>
537549
</SettingsProvider>,
@@ -571,7 +583,7 @@ describe('Canvas', () => {
571583
<SettingsProvider adapter={settingsAdapter}>
572584
<Provider>
573585
<VisualizationProvider controller={ControllerService.createController()}>
574-
<Canvas vizNodes={[vizNode]} entitiesCount={1} />
586+
<Canvas {...canvasPropsFromVizNodes([vizNode], 1)} />
575587
</VisualizationProvider>
576588
</Provider>
577589
</SettingsProvider>,

packages/ui/src/components/Visualization/Canvas/Canvas.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { useLocalStorage } from '../../../hooks';
3232
import { usePrevious } from '../../../hooks/previous.hook';
3333
import { LocalStorageKeys } from '../../../models';
3434
import { CanvasLayoutDirection } from '../../../models/settings/settings.model';
35-
import { IVisualizationNode } from '../../../models/visualization/base-visual-entity';
3635
import { SettingsContext } from '../../../providers/settings.provider';
3736
import { getInitialLayout } from '../../../utils/get-initial-layout';
3837
import { HorizontalLayoutIcon } from '../../Icons/HorizontalLayout';
@@ -43,20 +42,25 @@ import { applyCollapseState } from './apply-collapse-state';
4342
import { CanvasDefaults } from './canvas.defaults';
4443
import { CanvasEdge, CanvasNode, LayoutType } from './canvas.models';
4544
import { CanvasSideBar } from './CanvasSideBar';
46-
import { FlowService } from './flow.service';
4745

4846
interface CanvasProps {
49-
vizNodes: IVisualizationNode[];
47+
nodes: CanvasNode[];
48+
edges: CanvasEdge[];
5049
entitiesCount: number;
51-
isVizNodesResolving?: boolean;
50+
visibleEntitiesCount: number;
51+
isModelResolving?: boolean;
5252
contextToolbar?: ReactNode;
53+
applyCollapseOnUpdate?: boolean;
5354
}
5455

5556
export const Canvas: FunctionComponent<PropsWithChildren<CanvasProps>> = ({
56-
vizNodes,
57+
nodes,
58+
edges,
5759
entitiesCount,
58-
isVizNodesResolving = false,
60+
visibleEntitiesCount,
61+
isModelResolving = false,
5962
contextToolbar,
63+
applyCollapseOnUpdate = false,
6064
}) => {
6165
const settingsAdapter = useContext(SettingsContext);
6266
const settingsLayout = useMemo(
@@ -80,9 +84,9 @@ export const Canvas: FunctionComponent<PropsWithChildren<CanvasProps>> = ({
8084
const controller = useVisualizationController();
8185
const shouldShowEmptyState = useMemo(() => {
8286
const areNoFlows = entitiesCount === 0;
83-
const areAllFlowsHidden = vizNodes.length === 0 && entitiesCount > 0;
87+
const areAllFlowsHidden = visibleEntitiesCount === 0 && entitiesCount > 0;
8488
return areNoFlows || areAllFlowsHidden;
85-
}, [entitiesCount, vizNodes.length]);
89+
}, [entitiesCount, visibleEntitiesCount]);
8690

8791
const wasEmptyStateVisible = usePrevious(shouldShowEmptyState);
8892
const clearSelection = useCallback(() => {
@@ -96,22 +100,10 @@ export const Canvas: FunctionComponent<PropsWithChildren<CanvasProps>> = ({
96100
useEffect(() => {
97101
clearSelection();
98102

99-
if (isVizNodesResolving) {
103+
if (isModelResolving) {
100104
return;
101105
}
102106

103-
const nodes: CanvasNode[] = [];
104-
const edges: CanvasEdge[] = [];
105-
106-
vizNodes.forEach((vizNode) => {
107-
const { nodes: childNodes, edges: childEdges } = FlowService.getFlowDiagram(
108-
vizNode.getId() ?? vizNode.id,
109-
vizNode,
110-
);
111-
nodes.push(...childNodes);
112-
edges.push(...childEdges);
113-
});
114-
115107
const model: Model = {
116108
nodes,
117109
edges,
@@ -133,10 +125,12 @@ export const Canvas: FunctionComponent<PropsWithChildren<CanvasProps>> = ({
133125
}
134126

135127
controller.fromModel(model, true);
136-
applyCollapseState(controller);
128+
if (applyCollapseOnUpdate) {
129+
applyCollapseState(controller);
130+
}
137131
controller.getGraph().layout();
138132
// eslint-disable-next-line react-hooks/exhaustive-deps
139-
}, [controller, vizNodes, isVizNodesResolving]);
133+
}, [controller, nodes, edges, isModelResolving, applyCollapseOnUpdate]);
140134

141135
useEventListener<SelectionEventListener>(SELECTION_EVENT, setSelectedIds);
142136

@@ -238,7 +232,7 @@ export const Canvas: FunctionComponent<PropsWithChildren<CanvasProps>> = ({
238232

239233
const isSidebarOpen = useMemo(() => selectedIds.length > 0, [selectedIds.length]);
240234

241-
if (isVizNodesResolving) {
235+
if (isModelResolving) {
242236
return null;
243237
}
244238

packages/ui/src/components/Visualization/Canvas/controller.service.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { DagreGroupsLayout, ModelKind, Visualization } from '@patternfly/react-t
22

33
import { CustomGroupWithSelection } from '../Custom';
44
import { CustomEdge } from '../Custom/Edge/CustomEdge';
5+
import TopologyEdge from '../Custom/Edge/TopologyEdge';
56
import { PlaceholderNode } from '../Custom/Node/PlaceholderNode';
7+
import { TopologyNode } from '../Custom/Node/TopologyNode';
68
import { CanvasDefaults } from './canvas.defaults';
79
import { LayoutType } from './canvas.models';
810
import { ControllerService } from './controller.service';
@@ -75,6 +77,18 @@ describe('ControllerService', () => {
7577
expect(component).toBe(CustomEdge);
7678
});
7779

80+
it('should return TopologyNode for topology-node type', () => {
81+
const component = ControllerService.baselineComponentFactory(ModelKind.node, 'topology-node');
82+
83+
expect(component).toBe(TopologyNode);
84+
});
85+
86+
it('should return TopologyEdge for topology-edge type', () => {
87+
const component = ControllerService.baselineComponentFactory(ModelKind.edge, 'topology-edge');
88+
89+
expect(component).toBe(TopologyEdge);
90+
});
91+
7892
it('should return undefined for an unknown type', () => {
7993
const component = ControllerService.baselineComponentFactory({} as ModelKind, 'unknown');
8094

packages/ui/src/components/Visualization/Canvas/controller.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import {
1212

1313
import { CustomGroupWithSelection, CustomNodeWithSelection, NoBendpointsEdge } from '../Custom';
1414
import { CustomEdge } from '../Custom/Edge/CustomEdge';
15+
import TopologyEdge from '../Custom/Edge/TopologyEdge';
1516
import { CustomGraphWithSelection } from '../Custom/Graph/CustomGraph';
1617
import { PlaceholderNode } from '../Custom/Node/PlaceholderNode';
18+
import { TopologyNode } from '../Custom/Node/TopologyNode';
1719
import { LayoutType } from './canvas.models';
1820

1921
export class ControllerService {
@@ -52,6 +54,10 @@ export class ControllerService {
5254
return CustomGroupWithSelection;
5355
case 'node-placeholder':
5456
return PlaceholderNode;
57+
case 'topology-edge':
58+
return TopologyEdge;
59+
case 'topology-node':
60+
return TopologyNode;
5561
default:
5662
switch (kind) {
5763
case ModelKind.graph:

packages/ui/src/components/Visualization/Canvas/flow.service.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,37 @@ describe('FlowService', () => {
201201
expect(edges[1].target).toBe('test|route.from.steps.1.placeholder');
202202
});
203203
});
204+
205+
describe('getTopologyFlowDiagram', () => {
206+
it('builds one topology node per route and cross-route edges for matching in-vm endpoints', async () => {
207+
const producerRoute = await new CamelRouteVisualEntity({
208+
route: {
209+
id: 'route-producer',
210+
from: {
211+
uri: 'timer:tick',
212+
steps: [{ to: { uri: 'direct', parameters: { name: 'shared' } } }],
213+
},
214+
},
215+
}).toVizNode();
216+
const consumerRoute = await new CamelRouteVisualEntity({
217+
route: {
218+
id: 'route-consumer',
219+
from: { uri: 'direct', parameters: { name: 'shared' }, steps: [] },
220+
},
221+
}).toVizNode();
222+
223+
const { nodes, edges } = FlowService.getTopologyFlowDiagram([producerRoute, consumerRoute]);
224+
225+
expect(nodes).toHaveLength(2);
226+
expect(nodes.map((node) => node.id).sort()).toEqual(['route-consumer', 'route-producer']);
227+
expect(nodes.every((node) => node.type === 'topology-node')).toBe(true);
228+
229+
expect(edges).toHaveLength(1);
230+
expect(edges[0]).toMatchObject({
231+
type: 'topology-edge',
232+
source: 'route-producer',
233+
target: 'route-consumer',
234+
});
235+
});
236+
});
204237
});

0 commit comments

Comments
 (0)