Mirror of https://github.com/roostorg/osprey
github.com/roostorg/osprey
1import { memo, MutableRefObject, ReactElement, useEffect, useRef } from 'react';
2import * as ReactDOM from 'react-dom/client';
3import cytoscape, { Css, NodeSingular } from 'cytoscape';
4import dagre, { DagreLayoutOptions } from 'cytoscape-dagre';
5import popper from 'cytoscape-popper';
6import tippy from 'tippy.js';
7
8import { HierarchicalGraphOptions } from '../../types/RulesVisualizerTypes';
9
10import 'tippy.js/dist/tippy.css';
11import styles from './HierarchicalGraph.module.css';
12
13const defaultNodeStyle: Css.Node = {
14 'text-wrap': 'wrap',
15 'text-valign': 'bottom',
16 color: '#3c3c40',
17};
18
19const defaultEdgeStyle: Css.Edge = {
20 width: 1,
21 'target-arrow-shape': 'triangle',
22 'line-color': '#9dbaea',
23 'target-arrow-color': '#9dbaea',
24 'curve-style': 'bezier',
25};
26
27const defaultlayoutOptions: DagreLayoutOptions = {
28 name: 'dagre',
29 fit: true,
30 avoidOverlap: true,
31 nodeDimensionsIncludeLabels: true,
32 rankDir: 'LR',
33};
34
35cytoscape.use(dagre);
36cytoscape.use(popper);
37
38const HierarchicalGraph = ({
39 elements,
40 nodeStyle = {},
41 edgeStyle = {},
42 layoutOptions = {},
43 onLoad = () => {},
44 ToolTip,
45}: HierarchicalGraphOptions) => {
46 const containerRef = useRef(null);
47 const toolTipRef = useRef(null);
48 const tooltipRootRef = useRef<ReactDOM.Root | null>(null);
49
50 useEffect(() => {
51 const cy = cytoscape({
52 container: containerRef.current,
53 elements,
54 style: [
55 {
56 selector: 'node',
57 style: { ...defaultNodeStyle, ...nodeStyle },
58 },
59 {
60 selector: 'edge',
61 style: { ...defaultEdgeStyle, ...edgeStyle },
62 },
63 ],
64 layout: { ...defaultlayoutOptions, ...layoutOptions },
65 maxZoom: 10,
66 autoungrabify: true,
67 });
68 onLoad(cy);
69
70 let tip: any;
71 if (ToolTip) {
72 cy.nodes().bind('mouseover', (event) => {
73 tip = renderToolTipWithTippy(event.target as NodeSingular, ToolTip, containerRef, toolTipRef, tooltipRootRef);
74 });
75 cy.nodes().bind('mouseout', () => {
76 if (tip) {
77 tip.destroy();
78 tooltipRootRef.current?.unmount();
79 tooltipRootRef.current = null;
80 }
81 });
82 }
83
84 return () => {
85 if (tip) {
86 tip.destroy();
87 tooltipRootRef.current?.unmount();
88 tooltipRootRef.current = null;
89 }
90 cy.nodes().unbind('mouseover');
91 cy.nodes().unbind('mouseout');
92 cy.destroy();
93 };
94 }, [elements, nodeStyle, edgeStyle, layoutOptions, onLoad, ToolTip]);
95
96 return (
97 <>
98 <div ref={containerRef} className={styles.cyContainer} />
99 <div ref={toolTipRef} />
100 </>
101 );
102};
103
104function renderToolTipWithTippy(
105 node: NodeSingular,
106 ToolTip: ({ node }: { node: NodeSingular }) => ReactElement,
107 containerRef: MutableRefObject<null>,
108 toolTipRef: MutableRefObject<null>,
109 tooltipRootRef: MutableRefObject<ReactDOM.Root | null>
110) {
111 const popperRef = node.popperRef();
112 if (toolTipRef.current) {
113 tooltipRootRef.current = ReactDOM.createRoot(toolTipRef.current);
114 tooltipRootRef.current.render(<ToolTip node={node} />);
115 }
116 if (containerRef.current && toolTipRef.current) {
117 const tip: any = tippy(containerRef.current, {
118 getReferenceClientRect: popperRef.getBoundingClientRect,
119 content: toolTipRef.current,
120 placement: 'bottom',
121 arrow: true,
122 });
123 tip.show();
124 return tip;
125 }
126}
127
128export default memo(HierarchicalGraph, (prevProps, nextProps) => {
129 return (
130 JSON.stringify(prevProps.elements) == JSON.stringify(nextProps.elements) &&
131 prevProps.nodeStyle == nextProps.nodeStyle &&
132 prevProps.edgeStyle == nextProps.edgeStyle &&
133 prevProps.layoutOptions == nextProps.layoutOptions &&
134 prevProps.ToolTip == nextProps.ToolTip
135 );
136});