Mirror of https://github.com/roostorg/osprey
github.com/roostorg/osprey
1import { useState } from 'react';
2import { Alert, Button, Card, Spin, Switch } from 'antd';
3import { AimOutlined } from '@ant-design/icons';
4import shallow from 'zustand/shallow';
5import { Css, Core } from 'cytoscape';
6
7import useRulesVisualizerStore from '../../stores/RulesVisualizerStore';
8import HierarchicalGraph from './HierarchicalGraph';
9import RulesVisualizerHeader from './RulesVisualizerHeader';
10import { Node, NodeType, LabelType } from '../../types/RulesVisualizerTypes';
11
12import styles from './RulesVisualizer.module.css';
13import { getGraphJson } from '../../actions/RulesVisualizerActions';
14
15export const DEFAULT_ANIMATE_DURATION = 1000;
16
17const typeToShape: Record<string, Css.NodeShape> = {
18 [NodeType.Label]: 'ellipse',
19 [NodeType.Rule]: 'round-rectangle',
20};
21
22const nodeStyle: Css.Node = {
23 content: 'data(label)',
24 'background-color': (node) => getNodeColor(node.data('type'), node.data('label_type')),
25 shape: (node) => typeToShape[node.data('type')],
26};
27
28const ToolTip = ({ node }: { node: any }) => {
29 return <div>{node.data('file_path')}</div>;
30};
31
32const RulesVisualizerView = () => {
33 const [updateRuleVizGraph, nodes, edges, selectedFeature, selectedFeatureType, errorMessage] =
34 useRulesVisualizerStore(
35 (state) => [
36 state.updateRuleVizGraph,
37 state.nodes,
38 state.edges,
39 state.selectedFeature,
40 state.selectedFeatureType,
41 state.errorMessage,
42 ],
43 shallow
44 );
45 const [isLoading, setIsLoading] = useState(false);
46 const [showLabelUpstream, setShowLabelUpstream] = useState(false);
47 const [showLabelDownstream, setShowLabelDownstream] = useState(true);
48 const [cyto, setCyto] = useState<Core | null>(null);
49
50 const elements = {
51 nodes: (nodes || []).map((node) => ({
52 data: {
53 id: `${node.id}`,
54 label: getLabel(node),
55 type: node.type,
56 label_type: node.label_type,
57 label_name: node.label_name,
58 entity_name: node.entity_name,
59 file_path: node.file_path,
60 },
61 })),
62 edges: (edges || []).map((edge, idx) => ({
63 data: {
64 id: `edge-${idx}`,
65 source: `${edge.source}`,
66 target: `${edge.target}`,
67 },
68 })),
69 };
70
71 let alert;
72 if (nodes && !nodes.length) {
73 alert = <Alert className={styles.centered} type="warning" message="No associated nodes were found." />;
74 } else if (errorMessage) {
75 alert = <Alert className={styles.centered} type="error" message={`Error: ${errorMessage}. Please try again.`} />;
76 }
77
78 const onGraphLoad = (cy: Core) => {
79 // View defaults to fitting whole graph within viewport. Disable zooming out past that.
80 cy.minZoom(cy.zoom());
81 setCyto(cy);
82 };
83
84 const recenterOnClick = () => {
85 if (cyto) {
86 cyto.animate({
87 easing: 'ease-in-out',
88 duration: DEFAULT_ANIMATE_DURATION,
89 fit: { eles: cyto.elements(), padding: 0 },
90 });
91 }
92 };
93
94 const onShowLabelUpstreamToggle = async (checked: boolean) => {
95 setShowLabelUpstream(checked);
96 await rerenderLabelViewGraph(checked, showLabelDownstream);
97 };
98
99 const onShowLabelDownstreamToggle = async (checked: boolean) => {
100 setShowLabelDownstream(checked);
101 await rerenderLabelViewGraph(showLabelUpstream, checked);
102 };
103
104 const rerenderLabelViewGraph = async (show_upstream: boolean, show_downstream: boolean) => {
105 if (!selectedFeature || !selectedFeatureType) {
106 return;
107 }
108 setIsLoading(true);
109 updateRuleVizGraph({ nodes: null, edges: null });
110 const graphJson = await getGraphJson(
111 selectedFeatureType.toLowerCase(),
112 [`${selectedFeature}`],
113 show_upstream,
114 show_downstream
115 );
116 updateRuleVizGraph(graphJson);
117 setIsLoading(false);
118 };
119
120 return (
121 <div className={styles.viewContainer}>
122 <RulesVisualizerHeader
123 cy={cyto}
124 setIsLoading={setIsLoading}
125 showLabelUpstream={showLabelUpstream}
126 showLabelDownstream={showLabelDownstream}
127 />
128 <Spin className={styles.centered} size="large" spinning={isLoading} />
129 {alert}
130 <div className={styles.graphContainer}>
131 {selectedFeatureType === 'Label' && (
132 <Card size="small" title="Label View Filters" className={styles.recenterCard}>
133 <Switch onChange={onShowLabelUpstreamToggle} disabled={isLoading} defaultChecked={showLabelUpstream} /> Show
134 Upstream Nodes
135 <br></br>
136 <Switch
137 onChange={onShowLabelDownstreamToggle}
138 disabled={isLoading}
139 defaultChecked={showLabelDownstream}
140 />{' '}
141 Show Downstream Nodes
142 </Card>
143 )}
144 <HierarchicalGraph elements={elements} nodeStyle={nodeStyle} onLoad={onGraphLoad} ToolTip={ToolTip} />
145 {nodes && !!nodes.length && (
146 <Button
147 className={styles.recenterButton}
148 shape="circle"
149 icon={<AimOutlined />}
150 size="large"
151 onClick={recenterOnClick}
152 />
153 )}
154 </div>
155 </div>
156 );
157};
158
159function getLabel(node: Node) {
160 if (node.type === NodeType.Label) {
161 return `${node.type} [${getLabelTypeName(node.label_type)}]\n${node.label_name}\non ${
162 node.entity_name ? node.entity_name : 'Unassigned'
163 }`;
164 }
165 return `${node.type}\n${node.value}`;
166}
167
168function getLabelTypeName(label_type?: string) {
169 const labelTypeName = Object.entries(LabelType).find(([key, value]) => value === label_type);
170 return labelTypeName?.[0] || '';
171}
172
173function getNodeColor(type: string, label_type?: string) {
174 if (type === NodeType.Label && label_type == LabelType.Check) {
175 return '#DEA39E';
176 } else if (type === NodeType.Label && label_type == LabelType.Add) {
177 return '#D4DE9E';
178 } else if (type === NodeType.Label && label_type == LabelType.Remove) {
179 return '#BC916E';
180 } else if (type === NodeType.Rule) {
181 return '#CAE0F9';
182 }
183 return '#8F8F8F';
184}
185
186export default RulesVisualizerView;