@@ -9,58 +9,112 @@ import {
9
9
EuiToolTip ,
10
10
} from '@elastic/eui'
11
11
12
+ enum EntityType {
13
+ Node = 'Node' ,
14
+ Edge = 'Edge'
15
+ }
16
+
12
17
interface ISelectedEntityProps {
13
18
property : string
14
19
color : string
15
20
backgroundColor : string
16
21
props : { [ key : string ] : string | number | object }
22
+ type : EntityType
23
+ }
24
+
25
+ const isDarkTheme = document . body . classList . contains ( 'theme_DARK' )
26
+
27
+ const colorPicker = ( COLORS : Utils . IGoodColor [ ] , isDarkTheme : boolean ) => {
28
+ const color = new Utils . GoodColorPicker ( COLORS , isDarkTheme )
29
+ return ( label : string ) => color . getColor ( label )
17
30
}
18
31
19
- export default function Graph2 ( props : { graphKey : string , data : any [ ] } ) {
32
+ const labelColors = colorPicker ( Utils . NODE_COLORS , isDarkTheme )
33
+ const edgeColors = colorPicker ( Utils . EDGE_COLORS , isDarkTheme )
34
+
35
+ export default function Graph ( props : { graphKey : string , data : any [ ] } ) {
20
36
21
37
const d3Container = useRef < HTMLDivElement > ( )
22
38
const [ container , setContainer ] = useState ( null )
23
39
const [ selectedEntity , setSelectedEntity ] = useState < ISelectedEntityProps | null > ( null )
24
40
const [ start , setStart ] = useState ( false )
25
41
26
- const colorPicker = ( COLORS : Utils . IGoodColor [ ] ) => {
27
- const color = new Utils . GoodColorPicker ( COLORS )
28
- return ( label : string ) => color . getColor ( label )
29
- }
30
-
31
- const labelColors = colorPicker ( Utils . NODE_COLORS )
32
- const edgeColors = colorPicker ( Utils . EDGE_COLORS )
33
-
34
42
const parsedResponse = responseParser ( props . data )
35
- let nodeIds = parsedResponse . nodes . map ( n => n . id )
43
+ let nodeIds = new Set ( parsedResponse . nodes . map ( n => n . id ) )
44
+ let edgeIds = new Set ( parsedResponse . edges . map ( e => e . id ) )
36
45
let data = {
37
46
results : [ {
38
47
columns : parsedResponse . headers ,
39
48
data : [ {
40
49
graph : {
41
50
nodes : parsedResponse . nodes ,
42
- relationships : parsedResponse . edges . filter ( e => nodeIds . includes ( e . source ) && nodeIds . includes ( e . target ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
51
+ relationships : parsedResponse . edges . filter ( e => nodeIds . has ( e . source ) && nodeIds . has ( e . target ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
43
52
}
44
53
} ]
45
54
} ] ,
46
55
errors : [ ] ,
47
56
}
48
57
58
+ let nodeLabels = new Set ( parsedResponse . labels )
59
+ let edgeTypes = new Set ( parsedResponse . types )
60
+
49
61
const [ graphData , setGraphData ] = useState ( data )
50
62
51
63
useMemo ( async ( ) => {
52
64
53
65
let newGraphData = graphData
54
66
55
67
if ( parsedResponse . nodeIds . length > 0 ) {
56
- /* Fetch named path nodes */
57
- const resp = await globalThis . PluginSDK ?. executeRedisCommand ( `graph.ro_query "${ props . graphKey } " "MATCH (n) WHERE id(n) IN [${ parsedResponse . nodeIds } ] RETURN n"` ) ;
68
+ try {
69
+ /* Fetch named path nodes */
70
+ const resp = await globalThis . PluginSDK ?. executeRedisCommand ( `graph.ro_query "${ props . graphKey } " "MATCH (n) WHERE id(n) IN [${ [ ...parsedResponse . nodeIds ] } ] RETURN n"` ) ;
71
+
72
+ if ( Array . isArray ( resp ) && ( resp . length >= 1 || resp [ 0 ] . status === 'success' ) ) {
73
+ const parsedData = responseParser ( resp [ 0 ] . response )
74
+ parsedData . nodes . forEach ( n => {
75
+ nodeIds . add ( n . id )
76
+ n . labels . forEach ( nodeLabels . add , nodeLabels )
77
+ } )
78
+
79
+ parsedData . edges . forEach ( e => {
80
+ edgeTypes . add ( e . type )
81
+ } )
82
+
83
+ newGraphData = {
84
+ ...newGraphData ,
85
+ results : [
86
+ ...newGraphData . results ,
87
+ {
88
+ columns : parsedData . headers ,
89
+ data : [ {
90
+ graph : {
91
+ nodes : parsedData . nodes ,
92
+ relationships : parsedData . edges . filter ( e => nodeIds . has ( e . source ) && nodeIds . has ( e . target ) && ! edgeIds . has ( e . id ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
93
+ }
94
+ } ]
95
+ }
96
+ ]
97
+ }
98
+ }
99
+ } catch { }
100
+ }
101
+
102
+ try {
103
+ /* Fetch neighbours automatically */
104
+ const resp = await globalThis . PluginSDK ?. executeRedisCommand ( `graph.ro_query "${ props . graphKey } " "MATCH (n)-[t]->(m) WHERE ID(n) IN [${ [ ...nodeIds ] } ] OR ID(m) IN [${ [ ...nodeIds ] } ] RETURN DISTINCT t"` ) ;
58
105
59
106
if ( Array . isArray ( resp ) && ( resp . length >= 1 || resp [ 0 ] . status === 'success' ) ) {
60
107
const parsedData = responseParser ( resp [ 0 ] . response )
61
- nodeIds = [ ...new Set ( nodeIds . concat ( parsedData . nodes . map ( n => n . id ) ) ) ]
108
+ parsedData . nodes . forEach ( n => {
109
+ nodeIds . add ( n . id )
110
+ n . labels . forEach ( nodeLabels . add , nodeLabels )
111
+ } )
62
112
63
- newGraphData = {
113
+ parsedData . edges . forEach ( e => {
114
+ edgeTypes . add ( e . type )
115
+ } )
116
+
117
+ setGraphData ( {
64
118
...newGraphData ,
65
119
results : [
66
120
...newGraphData . results ,
@@ -69,43 +123,21 @@ export default function Graph2(props: { graphKey: string, data: any[] }) {
69
123
data : [ {
70
124
graph : {
71
125
nodes : parsedData . nodes ,
72
- relationships : parsedData . edges . filter ( e => nodeIds . includes ( e . source ) && nodeIds . includes ( e . target ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
126
+ /* TODO:
127
+ * track newly added edges
128
+ */
129
+ relationships : parsedData . edges . filter ( e => nodeIds . has ( e . source ) && nodeIds . has ( e . target ) && ! edgeIds . has ( e . id ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
73
130
}
74
131
} ]
75
132
}
76
133
]
77
- }
134
+ } )
78
135
}
79
- }
80
-
81
- /* Fetch neighbours automatically */
82
- const resp = await globalThis . PluginSDK ?. executeRedisCommand ( `graph.ro_query "${ props . graphKey } " "MATCH (n)-[t]->(m) WHERE ID(n) IN [${ nodeIds } ] OR ID(m) IN [${ nodeIds } ] RETURN DISTINCT t"` ) ;
83
-
84
- if ( Array . isArray ( resp ) && ( resp . length >= 1 || resp [ 0 ] . status === 'success' ) ) {
85
- const parsedData = responseParser ( resp [ 0 ] . response )
86
- nodeIds = [ ...new Set ( nodeIds . concat ( parsedData . nodes . map ( n => n . id ) ) ) ]
87
-
88
- setGraphData ( {
89
- ...newGraphData ,
90
- results : [
91
- ...newGraphData . results ,
92
- {
93
- columns : parsedData . headers ,
94
- data : [ {
95
- graph : {
96
- nodes : parsedData . nodes ,
97
- relationships : parsedData . edges . filter ( e => nodeIds . includes ( e . source ) && nodeIds . includes ( e . target ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
98
- }
99
- } ]
100
- }
101
- ]
102
- } )
103
- }
136
+ } catch { }
104
137
105
138
setStart ( true )
106
139
} , [ ] )
107
140
108
-
109
141
const zoom = d3 . zoom ( ) . scaleExtent ( [ 0 , 3 ] ) /* min, mac of zoom */
110
142
useEffect ( ( ) => {
111
143
if ( container != null ) return ;
@@ -143,7 +175,7 @@ export default function Graph2(props: { graphKey: string, data: any[] }) {
143
175
data : [ {
144
176
graph : {
145
177
nodes : parsedData . nodes ,
146
- relationships : parsedData . edges . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
178
+ relationships : parsedData . edges . filter ( e => ! edgeIds . has ( e . id ) ) . map ( e => ( { ...e , startNode : e . source , endNode : e . target } ) )
147
179
}
148
180
} ]
149
181
} ] ,
@@ -152,23 +184,27 @@ export default function Graph2(props: { graphKey: string, data: any[] }) {
152
184
} ,
153
185
onRelationshipDoubleClick ( relationship ) {
154
186
} ,
155
- onDisplayInfo : ( infoSvg , node ) => {
156
- let property ;
157
- let color ;
187
+ onDisplayInfo : ( infoSvg , entity ) => {
188
+ let property : string ;
189
+ let entityColor : Utils . IGoodColor ;
190
+ let t : EntityType ;
158
191
159
- if ( node . labels ) {
160
- [ property ] = node . labels ;
161
- color = labelColors ( property ) . color
192
+ if ( entity . labels ) {
193
+ [ property ] = entity . labels ;
194
+ entityColor = labelColors ( property )
195
+ t = EntityType . Node
162
196
} else {
163
- property = node . type ;
164
- color = edgeColors ( property ) . color
197
+ property = entity . type ;
198
+ entityColor = edgeColors ( property )
199
+ t = EntityType . Edge
165
200
}
166
201
167
202
setSelectedEntity ( {
168
203
property,
169
- backgroundColor : color ,
170
- props : { '<id>' : node . id , ...node . properties } ,
171
- color : graphd3 . invertColor ( color )
204
+ type : t ,
205
+ backgroundColor : entityColor . color ,
206
+ props : { '<id>' : entity . id , ...entity . properties } ,
207
+ color : entityColor . textColor
172
208
} )
173
209
} ,
174
210
zoomFit : false ,
@@ -180,55 +216,62 @@ export default function Graph2(props: { graphKey: string, data: any[] }) {
180
216
return (
181
217
< div className = "core-container" >
182
218
< div className = "d3-info" >
183
- {
184
- parsedResponse . nodes . length > 0 && (
185
- < div className = "d3-info-labels" >
219
+ < div className = "graph-legends" >
220
+ {
221
+ parsedResponse . nodes . length > 0 && (
222
+ < div className = "d3-info-labels" >
186
223
{
187
- parsedResponse . labels . map ( ( item , i ) => (
188
- < div className = "d3-info-item" key = { item + i } >
189
- < div className = "node-label" style = { { background : labelColors ( item ) . color } } > </ div >
190
- < div className = 'label-name' style = { { color : labelColors ( item ) . color } } > < code > { item } </ code > </ div >
224
+ [ ...nodeLabels ] . map ( ( item , i ) => (
225
+ < div
226
+ className = "box-node-label"
227
+ style = { { backgroundColor : labelColors ( item ) . color , color : labelColors ( item ) . textColor } }
228
+ key = { item + i }
229
+ >
230
+ { item }
191
231
</ div >
192
232
) )
193
233
}
194
234
</ div >
195
- )
196
- }
197
- {
198
- parsedResponse . edges . length > 0 && (
199
- < div className = "d3-info-labels" >
200
- {
201
- [ ...( new Set ( parsedResponse . edges . map ( e => e . type ) ) ) ] . map ( ( item , i ) => {
202
- return (
203
- < div key = { item + i . toString ( ) } className = "d3-info-item" >
204
- < div className = "edge-label-line" style = { { borderColor : edgeColors ( item ) . color } } > </ div >
205
- < div className = "edge-label-arrow" style = { { borderLeftColor : edgeColors ( item ) . color } } > </ div >
206
- < div className = "label-name" style = { { color : edgeColors ( item ) . color } } > { item } </ div >
235
+ )
236
+ }
237
+ {
238
+ parsedResponse . edges . length > 0 && (
239
+ < div className = "d3-info-labels" >
240
+ {
241
+ [ ...edgeTypes ] . map ( ( item , i ) => (
242
+ < div
243
+ key = { item + i . toString ( ) }
244
+ className = "box-edge-type"
245
+ style = { { borderColor : edgeColors ( item ) . color , color : edgeColors ( item ) . color } }
246
+ >
247
+ { item }
207
248
</ div >
208
- )
209
- } )
210
- }
211
- </ div >
212
- )
213
- }
249
+ ) )
250
+ }
251
+ </ div >
252
+ )
253
+ }
254
+ </ div >
214
255
{
215
256
selectedEntity &&
216
257
< div className = "info-component" >
217
258
< div className = "info-header" >
218
- < div className = "info-header-type" style = { { backgroundColor : selectedEntity . backgroundColor , color : selectedEntity . color } } > { selectedEntity . property } </ div >
219
- < EuiButtonIcon onClick = { ( ) => setSelectedEntity ( null ) } display = "empty" iconType = "cross" aria-label = "Close" />
259
+ {
260
+ selectedEntity . type === EntityType . Node ?
261
+ < div className = "box-node-label" style = { { backgroundColor : selectedEntity . backgroundColor , color : selectedEntity . color } } > { selectedEntity . property } </ div >
262
+ :
263
+ < div className = 'box-edge-type' style = { { borderColor : selectedEntity . backgroundColor , color : selectedEntity . backgroundColor } } > { selectedEntity . property } </ div >
264
+ }
265
+ < EuiButtonIcon color = "text" onClick = { ( ) => setSelectedEntity ( null ) } display = "empty" iconType = "cross" aria-label = "Close" />
220
266
</ div >
221
267
< div className = "info-props" >
222
- < table >
223
- {
224
- Object . keys ( selectedEntity . props ) . map ( k =>
225
- < tr >
226
- < td > { k } </ td >
227
- < td > { selectedEntity . props [ k ] } </ td >
228
- </ tr >
229
- )
230
- }
231
- </ table >
268
+ {
269
+ Object . keys ( selectedEntity . props ) . map ( k => [ k , selectedEntity . props [ k ] ] ) . reduce (
270
+ ( a , b ) => a . concat ( b ) , [ ]
271
+ ) . map ( k =>
272
+ < div > { k } </ div >
273
+ )
274
+ }
232
275
</ div >
233
276
</ div >
234
277
}
@@ -261,16 +304,6 @@ export default function Graph2(props: { graphKey: string, data: any[] }) {
261
304
onClick : ( ) => container . zoomFuncs . resetZoom ( ) ,
262
305
icon : 'bullseye'
263
306
} ,
264
- {
265
- name : 'Pan Left' ,
266
- onClick : ( ) => container . zoomFuncs . panLeft ( ) ,
267
- icon : 'editorItemAlignLeft'
268
- } ,
269
- {
270
- name : 'Pan Right' ,
271
- onClick : ( ) => container . zoomFuncs . panRight ( ) ,
272
- icon : 'editorItemAlignRight'
273
- } ,
274
307
{
275
308
name : 'Center' ,
276
309
onClick : ( ) => container . zoomFuncs . center ( ) ,
@@ -279,6 +312,7 @@ export default function Graph2(props: { graphKey: string, data: any[] }) {
279
312
] . map ( item => (
280
313
< EuiToolTip position = "left" content = { item . name } >
281
314
< EuiButtonIcon
315
+ color = 'text'
282
316
onClick = { item . onClick }
283
317
iconType = { item . icon }
284
318
aria-label = { item . name }
0 commit comments