Skip to content

Commit b5adb2c

Browse files
Merge pull request #1292 from aidanm3341/refactor-cytoscape
Removed zoom buttons and refactored CytoscapeRenderer
2 parents 6d89242 + 5ace20d commit b5adb2c

File tree

9 files changed

+292
-241
lines changed

9 files changed

+292
-241
lines changed

calm-hub-ui/src/visualizer/Visualizer.tsx

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ import './Visualizer.css';
33
import { Drawer } from './components/drawer/Drawer.js';
44
import { Navbar } from '../components/navbar/Navbar.js';
55
import React from 'react';
6-
import { ZoomProvider } from './components/zoom-context.provider.js';
7-
import { CALMArchitecture } from '../../../shared/src/types.js';
86
import { Menu } from './components/menu/Menu.js';
97
import { useLocation } from 'react-router-dom';
8+
import { CalmArchitectureSchema } from '@finos/calm-shared/src/types/core-types.js';
109

1110
function Visualizer() {
1211
const [title, setTitle] = useState<string | undefined>(undefined);
13-
const [instance, setCALMInstance] = useState<CALMArchitecture | undefined>(undefined);
12+
const [instance, setCALMInstance] = useState<CalmArchitectureSchema | undefined>(undefined);
1413
const [isConDescActive, setConDescActive] = React.useState(true);
1514
const [isNodeDescActive, setNodeDescActive] = React.useState(true);
1615
const location = useLocation();
@@ -32,25 +31,23 @@ function Visualizer() {
3231
}, [fileInstance, fileTitle, data]);
3332

3433
return (
35-
<ZoomProvider>
36-
<div className="h-screen flex flex-col">
37-
<Navbar />
38-
<Menu
39-
handleUpload={handleFile}
40-
isGraphRendered={!!instance}
41-
toggleNodeDesc={toggleState(setNodeDescActive)}
42-
toggleConnectionDesc={toggleState(setConDescActive)}
43-
isNodeDescActive={isNodeDescActive}
44-
isConDescActive={isConDescActive}
45-
/>
46-
<Drawer
47-
isNodeDescActive={isNodeDescActive}
48-
isConDescActive={isConDescActive}
49-
calmInstance={instance}
50-
title={title}
51-
/>
52-
</div>
53-
</ZoomProvider>
34+
<div className="h-screen flex flex-col">
35+
<Navbar />
36+
<Menu
37+
handleUpload={handleFile}
38+
isGraphRendered={!!instance}
39+
toggleNodeDesc={toggleState(setNodeDescActive)}
40+
toggleConnectionDesc={toggleState(setConDescActive)}
41+
isNodeDescActive={isNodeDescActive}
42+
isConDescActive={isConDescActive}
43+
/>
44+
<Drawer
45+
isNodeDescActive={isNodeDescActive}
46+
isConDescActive={isConDescActive}
47+
calmInstance={instance}
48+
title={title}
49+
/>
50+
</div>
5451
);
5552
}
5653

Lines changed: 76 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import './cytoscape.css';
2-
import { useContext, useEffect, useRef, useState } from 'react';
3-
import cytoscape, { Core, EdgeSingular, NodeSingular } from 'cytoscape';
2+
import { useEffect, useRef } from 'react';
3+
import cytoscape, { EdgeSingular, NodeSingular } from 'cytoscape';
44
import nodeEdgeHtmlLabel from 'cytoscape-node-edge-html-label';
55
import expandCollapse from 'cytoscape-expand-collapse';
6-
import { Sidebar } from '../sidebar/Sidebar.js';
7-
import { ZoomContext } from '../zoom-context.provider.js';
86
import { Edge, CalmNode } from '../../contracts/contracts.js';
97
import { LayoutCorrectionService } from '../../services/layout-correction-service.js';
108

@@ -24,81 +22,81 @@ const breadthFirstLayout = {
2422
spacingFactor: 1.25,
2523
};
2624

27-
interface Props {
28-
title?: string;
25+
export interface CytoscapeRendererProps {
2926
isNodeDescActive: boolean;
30-
isConDescActive: boolean;
27+
isRelationshipDescActive: boolean;
3128
nodes: CalmNode[];
3229
edges: Edge[];
30+
nodeClickedCallback: (x: CalmNode['data'] | Edge['data']) => void;
31+
edgeClickedCallback: (x: CalmNode['data'] | Edge['data']) => void;
3332
}
3433

35-
export const CytoscapeRenderer = ({
36-
title,
37-
nodes = [],
38-
edges = [],
39-
isConDescActive,
40-
isNodeDescActive,
41-
}: Props) => {
42-
const cyRef = useRef<HTMLDivElement>(null);
43-
const [cy, setCy] = useState<Core | null>(null);
44-
const { zoomLevel, updateZoom } = useContext(ZoomContext);
45-
const [selectedItem, setSelectedItem] = useState<CalmNode['data'] | Edge['data'] | null>(null);
46-
47-
const layoutCorrectionService = new LayoutCorrectionService();
48-
49-
// Generate node label templates
50-
const getNodeLabelTemplateGenerator =
51-
(selected = false) =>
52-
(data: CalmNode['data']) => `
34+
function getNodeLabelTemplateGenerator(selected: boolean, includeDescription: boolean) {
35+
return (data: CalmNode['data']) => `
5336
<div class="node element ${selected ? 'selected-node' : ''}">
5437
<p class="title">${data.label}</p>
5538
<p class="type">${data.type}</p>
56-
<p class="description">${isNodeDescActive ? data.description : ''}</p>
39+
<p class="description">${includeDescription ? data.description : ''}</p>
5740
</div>
5841
`;
42+
}
43+
44+
function getEdgeStyle(showDescription: boolean): cytoscape.Css.Edge {
45+
return {
46+
width: 2,
47+
'curve-style': 'bezier',
48+
label: showDescription ? 'data(label)' : '',
49+
'target-arrow-shape': 'triangle',
50+
'text-wrap': 'ellipsis',
51+
'text-background-color': 'white',
52+
'text-background-opacity': 1,
53+
'text-background-padding': '5px',
54+
};
55+
}
56+
57+
function getNodeStyle(showDescription: boolean): cytoscape.Css.Node {
58+
return {
59+
label: showDescription
60+
? 'data(_displayPlaceholderWithDesc)'
61+
: 'data(_displayPlaceholderWithoutDesc)',
62+
'text-valign': 'center',
63+
'text-halign': 'center',
64+
'text-wrap': 'wrap',
65+
'text-max-width': '180px',
66+
'font-family': 'Arial',
67+
width: '200px',
68+
height: 'label',
69+
shape: 'rectangle',
70+
};
71+
}
72+
73+
const layoutCorrectionService = new LayoutCorrectionService();
74+
75+
export function CytoscapeRenderer({
76+
nodes = [],
77+
edges = [],
78+
isRelationshipDescActive,
79+
isNodeDescActive,
80+
nodeClickedCallback,
81+
edgeClickedCallback,
82+
}: CytoscapeRendererProps) {
83+
const cyRef = useRef<HTMLDivElement>(null);
5984

60-
// Initialize Cytoscape instance
6185
useEffect(() => {
6286
const container = cyRef.current;
6387
if (!container) return;
6488

65-
// Preserve zoom and pan state if Cytoscape instance already exists
66-
const currentZoom = cy?.zoom() || 1;
67-
const currentPan = cy?.pan() || { x: 0, y: 0 };
68-
69-
// Initialize Cytoscape
70-
const updatedCy = cytoscape({
89+
const cy = cytoscape({
7190
container,
7291
elements: [...nodes, ...edges],
7392
style: [
7493
{
7594
selector: 'edge',
76-
style: {
77-
width: 2,
78-
'curve-style': 'bezier',
79-
label: isConDescActive ? 'data(label)' : '',
80-
'target-arrow-shape': 'triangle',
81-
'text-wrap': 'ellipsis',
82-
'text-background-color': 'white',
83-
'text-background-opacity': 1,
84-
'text-background-padding': '5px',
85-
},
95+
style: getEdgeStyle(isRelationshipDescActive),
8696
},
8797
{
8898
selector: 'node',
89-
style: {
90-
label: isNodeDescActive
91-
? 'data(_displayPlaceholderWithDesc)'
92-
: 'data(_displayPlaceholderWithoutDesc)',
93-
'text-valign': 'center',
94-
'text-halign': 'center',
95-
'text-wrap': 'wrap',
96-
'text-max-width': '180px',
97-
'font-family': 'Arial',
98-
width: '200px',
99-
height: 'label',
100-
shape: 'rectangle',
101-
},
99+
style: getNodeStyle(isNodeDescActive),
102100
},
103101
{
104102
selector: ':parent',
@@ -113,67 +111,43 @@ export const CytoscapeRenderer = ({
113111
maxZoom: 5,
114112
});
115113

116-
// Restore zoom and pan state
117-
updatedCy.zoom(currentZoom);
118-
updatedCy.pan(currentPan);
119-
120-
// Add event listeners
121-
updatedCy.on('tap', 'node', (e) => {
114+
cy.on('tap', 'node', (e) => {
122115
const node = e.target as NodeSingular;
123-
setSelectedItem(node?.data());
116+
nodeClickedCallback(node?.data());
124117
});
125118

126-
updatedCy.on('tap', 'edge', (e) => {
119+
cy.on('tap', 'edge', (e) => {
127120
const edge = e.target as EdgeSingular;
128-
setSelectedItem(edge?.data());
121+
edgeClickedCallback(edge?.data());
129122
});
130123

131-
updatedCy.on('zoom', () => updateZoom(updatedCy.zoom()));
132-
133-
// Update node labels dynamically
134-
/* eslint-disable @typescript-eslint/no-explicit-any */
135-
(updatedCy as Core & { nodeHtmlLabel: any }).nodeHtmlLabel([
124+
// This function comes from a plugin which doesn't have proper types, which is why the hacky casting is needed
125+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
126+
(cy as unknown as any).nodeHtmlLabel([
136127
{
137128
query: '.node',
138129
valign: 'top',
139130
valignBox: 'top',
140-
tpl: getNodeLabelTemplateGenerator(false),
131+
tpl: getNodeLabelTemplateGenerator(false, isNodeDescActive),
141132
},
142133
{
143134
query: '.node:selected',
144135
valign: 'top',
145136
valignBox: 'top',
146-
tpl: getNodeLabelTemplateGenerator(true),
137+
tpl: getNodeLabelTemplateGenerator(true, isNodeDescActive),
147138
},
148139
]);
149-
layoutCorrectionService.calculateAndUpdateNodePositions(updatedCy, nodes);
150-
// Set Cytoscape instance
151-
setCy(updatedCy);
152-
153-
return () => {
154-
updatedCy.destroy(); // Clean up Cytoscape instance
155-
};
156-
}, [nodes, edges, isConDescActive, isNodeDescActive, updateZoom]);
157-
158-
// Synchronize zoom level with context
159-
useEffect(() => {
160-
if (cy && cy.zoom() !== zoomLevel) {
161-
cy.zoom(zoomLevel);
162-
}
163-
}, [cy, zoomLevel]);
164140

165-
return (
166-
<div className="relative flex m-auto border">
167-
{title && (
168-
<div className="graph-title absolute m-5 bg-accent shadow-md">
169-
<span className="text-m font-thin text-primary-content">Architecture: </span>
170-
<span className="text-m font-semibold text-primary-content">{title}</span>
171-
</div>
172-
)}
173-
<div ref={cyRef} className="flex-1 bg-white visualizer" style={{ height: '100vh' }} />
174-
{selectedItem && (
175-
<Sidebar selectedData={selectedItem} closeSidebar={() => setSelectedItem(null)} />
176-
)}
177-
</div>
178-
);
179-
};
141+
layoutCorrectionService.calculateAndUpdateNodePositions(cy, nodes);
142+
}, [
143+
nodes,
144+
edges,
145+
isNodeDescActive,
146+
isRelationshipDescActive,
147+
nodeClickedCallback,
148+
edgeClickedCallback,
149+
cyRef,
150+
]);
151+
152+
return <div ref={cyRef} className="flex-1 bg-white visualizer" style={{ height: '100vh' }} />;
153+
}

calm-hub-ui/src/visualizer/components/drawer/Drawer.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Sidebar } from '../sidebar/Sidebar.js';
22
import { useState } from 'react';
3-
import { CytoscapeRenderer } from '../cytoscape-renderer/CytoscapeRenderer.js';
43
import {
54
CalmArchitectureSchema,
65
CalmRelationshipSchema,
@@ -12,6 +11,7 @@ import {
1211
CALMInteractsRelationship,
1312
} from '../../../../../shared/src/types.js';
1413
import { CalmNode, Edge } from '../../contracts/contracts.js';
14+
import { VisualizerContainer } from '../visualizer-container/VisualizerContainer.js';
1515

1616
interface DrawerProps {
1717
calmInstance?: CalmArchitectureSchema;
@@ -132,8 +132,8 @@ export function Drawer({ calmInstance, title, isConDescActive, isNodeDescActive
132132
composedOfRel?.type === 'child' && composedOfRel.parent
133133
? composedOfRel.parent
134134
: deployedInRel?.type === 'child' && deployedInRel.parent
135-
? deployedInRel.parent
136-
: undefined;
135+
? deployedInRel.parent
136+
: undefined;
137137

138138
if (parentId) {
139139
newData.data.parent = parentId;
@@ -189,8 +189,8 @@ export function Drawer({ calmInstance, title, isConDescActive, isNodeDescActive
189189
/>
190190
<div className="drawer-content">
191191
{calmInstance ? (
192-
<CytoscapeRenderer
193-
isConDescActive={isConDescActive}
192+
<VisualizerContainer
193+
isRelationshipDescActive={isConDescActive}
194194
isNodeDescActive={isNodeDescActive}
195195
title={title}
196196
nodes={nodes}

0 commit comments

Comments
 (0)