11import './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' ;
44import nodeEdgeHtmlLabel from 'cytoscape-node-edge-html-label' ;
55import expandCollapse from 'cytoscape-expand-collapse' ;
6- import { Sidebar } from '../sidebar/Sidebar.js' ;
7- import { ZoomContext } from '../zoom-context.provider.js' ;
86import { Edge , CalmNode } from '../../contracts/contracts.js' ;
97import { 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+ }
0 commit comments