1
+ import { Banner , Dialog , Flex , IconButtonArray , LoadingSpinner } from '@neo4j-ndl/react' ;
2
+ import { useCallback , useEffect , useRef , useState } from 'react' ;
3
+ import { GraphType , GraphViewModalProps , OptionType , Scheme , UserCredentials } from '../../types' ;
4
+ import { InteractiveNvlWrapper } from '@neo4j-nvl/react' ;
5
+ import NVL from '@neo4j-nvl/base' ;
6
+ import type { Node , Relationship } from '@neo4j-nvl/base' ;
7
+ import { Resizable } from 're-resizable' ;
8
+ import {
9
+ ArrowPathIconOutline ,
10
+ DragIcon ,
11
+ FitToScreenIcon ,
12
+ MagnifyingGlassMinusIconOutline ,
13
+ MagnifyingGlassPlusIconOutline ,
14
+ } from '@neo4j-ndl/react/icons' ;
15
+ import IconButtonWithToolTip from '../UI/IconButtonToolTip' ;
16
+ import { filterData , processGraphData } from '../../utils/Utils' ;
17
+ import { useCredentials } from '../../context/UserCredentials' ;
18
+ import { LegendsChip } from './LegendsChip' ;
19
+ import graphQueryAPI from '../../services/GraphQuery' ;
20
+ import {
21
+ entityGraph ,
22
+ graphQuery ,
23
+ graphView ,
24
+ intitalGraphType ,
25
+ knowledgeGraph ,
26
+ lexicalGraph ,
27
+ mouseEventCallbacks ,
28
+ nvlOptions ,
29
+ queryMap ,
30
+ } from '../../utils/Constants' ;
31
+ // import CheckboxSelection from './CheckboxSelection';
32
+ import DropdownComponent from '../Dropdown' ;
33
+ const GraphViewModal : React . FunctionComponent < GraphViewModalProps > = ( {
34
+ open,
35
+ inspectedName,
36
+ setGraphViewOpen,
37
+ viewPoint,
38
+ nodeValues,
39
+ relationshipValues,
40
+ selectedRows,
41
+ } ) => {
42
+ const nvlRef = useRef < NVL > ( null ) ;
43
+ const [ nodes , setNodes ] = useState < Node [ ] > ( [ ] ) ;
44
+ const [ relationships , setRelationships ] = useState < Relationship [ ] > ( [ ] ) ;
45
+ const [ graphType , setGraphType ] = useState < GraphType [ ] > ( intitalGraphType ) ;
46
+ const [ allNodes , setAllNodes ] = useState < Node [ ] > ( [ ] ) ;
47
+ const [ allRelationships , setAllRelationships ] = useState < Relationship [ ] > ( [ ] ) ;
48
+ const [ loading , setLoading ] = useState < boolean > ( false ) ;
49
+ const [ status , setStatus ] = useState < 'unknown' | 'success' | 'danger' > ( 'unknown' ) ;
50
+ const [ statusMessage , setStatusMessage ] = useState < string > ( '' ) ;
51
+ const { userCredentials } = useCredentials ( ) ;
52
+ const [ scheme , setScheme ] = useState < Scheme > ( { } ) ;
53
+ const [ newScheme , setNewScheme ] = useState < Scheme > ( { } ) ;
54
+ const [ dropdownVal , setDropdownVal ] = useState < OptionType > ( {
55
+ label : 'Knowledge Graph' ,
56
+ value : queryMap . DocChunkEntities ,
57
+ } ) ;
58
+
59
+ // const handleCheckboxChange = (graph: GraphType) => {
60
+ // const currentIndex = graphType.indexOf(graph);
61
+ // const newGraphSelected = [...graphType];
62
+ // if (currentIndex === -1) {
63
+ // newGraphSelected.push(graph);
64
+ // } else {
65
+ // newGraphSelected.splice(currentIndex, 1);
66
+ // }
67
+ // setGraphType(newGraphSelected);
68
+ // };
69
+
70
+ const handleZoomToFit = ( ) => {
71
+ nvlRef . current ?. fit (
72
+ allNodes . map ( ( node ) => node . id ) ,
73
+ { }
74
+ ) ;
75
+ } ;
76
+
77
+ // Destroy the component
78
+ useEffect ( ( ) => {
79
+ const timeoutId = setTimeout ( ( ) => {
80
+ handleZoomToFit ( ) ;
81
+ } , 10 ) ;
82
+ return ( ) => {
83
+ nvlRef . current ?. destroy ( ) ;
84
+ setGraphType ( intitalGraphType ) ;
85
+ clearTimeout ( timeoutId ) ;
86
+ setScheme ( { } ) ;
87
+ setNodes ( [ ] ) ;
88
+ setRelationships ( [ ] ) ;
89
+ setAllNodes ( [ ] ) ;
90
+ setAllRelationships ( [ ] ) ;
91
+ setDropdownVal ( { label : 'Knowledge Graph' , value : queryMap . DocChunkEntities } ) ;
92
+ } ;
93
+ } , [ ] ) ;
94
+
95
+ // To get nodes and relations on basis of view
96
+ const fetchData = useCallback ( async ( ) => {
97
+ try {
98
+ const nodeRelationshipData =
99
+ viewPoint === 'showGraphView'
100
+ ? await graphQueryAPI (
101
+ userCredentials as UserCredentials ,
102
+ graphQuery ,
103
+ selectedRows ?. map ( ( f ) => f . name )
104
+ )
105
+ : await graphQueryAPI ( userCredentials as UserCredentials , graphQuery , [ inspectedName ?? '' ] ) ;
106
+ return nodeRelationshipData ;
107
+ } catch ( error : any ) {
108
+ console . log ( error ) ;
109
+ }
110
+ } , [ viewPoint , selectedRows , graphQuery , inspectedName , userCredentials ] ) ;
111
+
112
+ // Api call to get the nodes and relations
113
+ const graphApi = async ( ) => {
114
+ try {
115
+ const result = await fetchData ( ) ;
116
+ if ( result && result . data . data . nodes . length > 0 ) {
117
+ const neoNodes = result . data . data . nodes . map ( ( f : Node ) => f ) ;
118
+ const neoRels = result . data . data . relationships . map ( ( f : Relationship ) => f ) ;
119
+ const { finalNodes, finalRels, schemeVal } = processGraphData ( neoNodes , neoRels ) ;
120
+ setAllNodes ( finalNodes ) ;
121
+ setAllRelationships ( finalRels ) ;
122
+ setScheme ( schemeVal ) ;
123
+ setNodes ( finalNodes ) ;
124
+ setRelationships ( finalRels ) ;
125
+ setNewScheme ( schemeVal ) ;
126
+ setLoading ( false ) ;
127
+ } else {
128
+ setLoading ( false ) ;
129
+ setStatus ( 'danger' ) ;
130
+ setStatusMessage ( `No Nodes and Relations for the ${ inspectedName } file` ) ;
131
+ }
132
+ } catch ( error : any ) {
133
+ setLoading ( false ) ;
134
+ setStatus ( 'danger' ) ;
135
+ setStatusMessage ( error . message ) ;
136
+ }
137
+ } ;
138
+
139
+ useEffect ( ( ) => {
140
+ if ( open ) {
141
+ setLoading ( true ) ;
142
+ if ( viewPoint !== 'chatInfoView' ) {
143
+ graphApi ( ) ;
144
+ } else {
145
+ const { finalNodes, finalRels, schemeVal } = processGraphData ( nodeValues ?? [ ] , relationshipValues ?? [ ] ) ;
146
+ setAllNodes ( finalNodes ) ;
147
+ setAllRelationships ( finalRels ) ;
148
+ setScheme ( schemeVal ) ;
149
+ setNodes ( finalNodes ) ;
150
+ setRelationships ( finalRels ) ;
151
+ setNewScheme ( schemeVal ) ;
152
+ setLoading ( false ) ;
153
+ }
154
+ }
155
+ } , [ open ] ) ;
156
+
157
+ if ( ! open ) {
158
+ return < > </ > ;
159
+ }
160
+
161
+ const headerTitle =
162
+ viewPoint === 'showGraphView' || viewPoint === 'chatInfoView'
163
+ ? 'Generated Graph'
164
+ : `Inspect Generated Graph from ${ inspectedName } ` ;
165
+
166
+ const dropDownView = viewPoint !== 'chatInfoView' ;
167
+
168
+ const nvlCallbacks = {
169
+ onLayoutComputing ( isComputing : boolean ) {
170
+ if ( ! isComputing ) {
171
+ handleZoomToFit ( ) ;
172
+ }
173
+ } ,
174
+ } ;
175
+
176
+ // To handle the current zoom in function of graph
177
+ const handleZoomIn = ( ) => {
178
+ nvlRef . current ?. setZoom ( nvlRef . current . getScale ( ) * 1.3 ) ;
179
+ } ;
180
+
181
+ // To handle the current zoom out function of graph
182
+ const handleZoomOut = ( ) => {
183
+ nvlRef . current ?. setZoom ( nvlRef . current . getScale ( ) * 0.7 ) ;
184
+ } ;
185
+
186
+ // Refresh the graph with nodes and relations if file is processing
187
+ const handleRefresh = ( ) => {
188
+ graphApi ( ) ;
189
+ setGraphType ( intitalGraphType ) ;
190
+ setDropdownVal ( { label : 'Knowledge Graph' , value : queryMap . DocChunkEntities } ) ;
191
+ } ;
192
+
193
+ // when modal closes reset all states to default
194
+ const onClose = ( ) => {
195
+ setStatus ( 'unknown' ) ;
196
+ setStatusMessage ( '' ) ;
197
+ setGraphViewOpen ( false ) ;
198
+ setScheme ( { } ) ;
199
+ setGraphType ( intitalGraphType ) ;
200
+ setNodes ( [ ] ) ;
201
+ setRelationships ( [ ] ) ;
202
+ setDropdownVal ( { label : 'Knowledge Graph' , value : queryMap . DocChunkEntities } ) ;
203
+ } ;
204
+
205
+ // sort the legends in with Chunk and Document always the first two values
206
+ const legendCheck = Object . keys ( newScheme ) . sort ( ( a , b ) => {
207
+ if ( a === 'Document' || a === 'Chunk' ) {
208
+ return - 1 ;
209
+ } else if ( b === 'Document' || b === 'Chunk' ) {
210
+ return 1 ;
211
+ }
212
+ return a . localeCompare ( b ) ;
213
+ } ) ;
214
+
215
+ // setting the default dropdown values
216
+ const getDropdownDefaultValue = ( ) => {
217
+ if ( graphType . includes ( 'Document' ) && graphType . includes ( 'Chunk' ) && graphType . includes ( 'Entities' ) ) {
218
+ return knowledgeGraph ;
219
+ }
220
+ if ( graphType . includes ( 'Document' ) && graphType . includes ( 'Chunk' ) ) {
221
+ return lexicalGraph ;
222
+ }
223
+ if ( graphType . includes ( 'Entities' ) ) {
224
+ return entityGraph ;
225
+ }
226
+ return '' ;
227
+ } ;
228
+
229
+ // Make a function call to store the nodes and relations in their states
230
+ const initGraph = ( graphType : GraphType [ ] , finalNodes : Node [ ] , finalRels : Relationship [ ] , schemeVal : Scheme ) => {
231
+ if ( allNodes . length > 0 && allRelationships . length > 0 ) {
232
+ const { filteredNodes, filteredRelations, filteredScheme } = filterData (
233
+ graphType ,
234
+ finalNodes ?? [ ] ,
235
+ finalRels ?? [ ] ,
236
+ schemeVal
237
+ ) ;
238
+ setNodes ( filteredNodes ) ;
239
+ setRelationships ( filteredRelations ) ;
240
+ setNewScheme ( filteredScheme ) ;
241
+ }
242
+ } ;
243
+
244
+ // handle dropdown value change and call the init graph method
245
+ const handleDropdownChange = ( selectedOption : OptionType | null | void ) => {
246
+ if ( selectedOption ?. value ) {
247
+ const selectedValue = selectedOption . value ;
248
+ let newGraphType : GraphType [ ] = [ ] ;
249
+ if ( selectedValue === 'entities' ) {
250
+ newGraphType = [ 'Entities' ] ;
251
+ } else if ( selectedValue === queryMap . DocChunks ) {
252
+ newGraphType = [ 'Document' , 'Chunk' ] ;
253
+ } else if ( selectedValue === queryMap . DocChunkEntities ) {
254
+ newGraphType = [ 'Document' , 'Entities' , 'Chunk' ] ;
255
+ }
256
+ setGraphType ( newGraphType ) ;
257
+ setDropdownVal ( selectedOption ) ;
258
+ initGraph ( newGraphType , allNodes , allRelationships , scheme ) ;
259
+ }
260
+ } ;
261
+ return (
262
+ < >
263
+ < Dialog
264
+ modalProps = { {
265
+ className : 'h-[90%]' ,
266
+ id : 'default-menu' ,
267
+ } }
268
+ size = 'unset'
269
+ open = { open }
270
+ aria-labelledby = 'form-dialog-title'
271
+ disableCloseButton = { false }
272
+ onClose = { onClose }
273
+ >
274
+ < Dialog . Header id = 'graph-title' >
275
+ { headerTitle }
276
+ < Flex className = 'w-full' alignItems = 'center' justifyContent = 'flex-end' flexDirection = 'row' >
277
+ { /* {checkBoxView && (
278
+ <CheckboxSelection graphType={graphType} loading={loading} handleChange={handleCheckboxChange} />
279
+ )} */ }
280
+ { dropDownView && (
281
+ < DropdownComponent
282
+ onSelect = { handleDropdownChange }
283
+ options = { graphView }
284
+ placeholder = 'Select Graph Type'
285
+ defaultValue = { getDropdownDefaultValue ( ) }
286
+ view = 'GraphView'
287
+ isDisabled = { loading }
288
+ value = { dropdownVal }
289
+ />
290
+ ) }
291
+ </ Flex >
292
+ </ Dialog . Header >
293
+ < Dialog . Content className = 'flex flex-col n-gap-token-4 w-full grow overflow-auto border border-palette-neutral-border-weak' >
294
+ < div className = 'bg-white relative w-full h-full max-h-full' >
295
+ { loading ? (
296
+ < div className = 'my-40 flex items-center justify-center' >
297
+ < LoadingSpinner size = 'large' />
298
+ </ div >
299
+ ) : status !== 'unknown' ? (
300
+ < div className = 'my-40 flex items-center justify-center' >
301
+ < Banner name = 'graph banner' description = { statusMessage } type = { status } />
302
+ </ div >
303
+ ) : nodes . length === 0 || relationships . length === 0 ? (
304
+ < div className = 'my-40 flex items-center justify-center' >
305
+ < Banner name = 'graph banner' description = 'No Entities Found' type = 'danger' />
306
+ </ div >
307
+ ) : (
308
+ < >
309
+ < div className = 'flex' style = { { height : '100%' } } >
310
+ < div className = 'bg-palette-neutral-bg-default relative' style = { { width : '100%' , flex : '1' } } >
311
+ < InteractiveNvlWrapper
312
+ nodes = { nodes }
313
+ rels = { relationships }
314
+ nvlOptions = { nvlOptions }
315
+ ref = { nvlRef }
316
+ mouseEventCallbacks = { { ...mouseEventCallbacks } }
317
+ interactionOptions = { {
318
+ selectOnClick : true ,
319
+ } }
320
+ nvlCallbacks = { nvlCallbacks }
321
+ />
322
+ < IconButtonArray orientation = 'vertical' floating className = 'absolute bottom-4 right-4' >
323
+ { viewPoint !== 'chatInfoView' && (
324
+ < IconButtonWithToolTip
325
+ label = 'Refresh'
326
+ text = 'Refresh graph'
327
+ onClick = { handleRefresh }
328
+ placement = 'left'
329
+ >
330
+ < ArrowPathIconOutline />
331
+ </ IconButtonWithToolTip >
332
+ ) }
333
+ < IconButtonWithToolTip label = 'Zoomin' text = 'Zoom in' onClick = { handleZoomIn } placement = 'left' >
334
+ < MagnifyingGlassPlusIconOutline />
335
+ </ IconButtonWithToolTip >
336
+ < IconButtonWithToolTip label = 'Zoom out' text = 'Zoom out' onClick = { handleZoomOut } placement = 'left' >
337
+ < MagnifyingGlassMinusIconOutline />
338
+ </ IconButtonWithToolTip >
339
+ < IconButtonWithToolTip
340
+ label = 'Zoom to fit'
341
+ text = 'Zoom to fit'
342
+ onClick = { handleZoomToFit }
343
+ placement = 'left'
344
+ >
345
+ < FitToScreenIcon />
346
+ </ IconButtonWithToolTip >
347
+ </ IconButtonArray >
348
+ </ div >
349
+ < Resizable
350
+ defaultSize = { {
351
+ width : 400 ,
352
+ height : '100%' ,
353
+ } }
354
+ minWidth = { 230 }
355
+ maxWidth = '72%'
356
+ enable = { {
357
+ top : false ,
358
+ right : false ,
359
+ bottom : false ,
360
+ left : true ,
361
+ topRight : false ,
362
+ bottomRight : false ,
363
+ bottomLeft : false ,
364
+ topLeft : false ,
365
+ } }
366
+ handleComponent = { { left : < DragIcon className = 'absolute top-1/2 h-6 w-6' /> } }
367
+ handleClasses = { { left : 'ml-1' } }
368
+ >
369
+ < div className = 'legend_div' >
370
+ < h4 className = 'py-4 pt-3 ml-2' > Result Overview</ h4 >
371
+ < div className = 'flex gap-2 flex-wrap ml-2' >
372
+ { legendCheck . map ( ( key , index ) => (
373
+ < LegendsChip key = { index } title = { key } scheme = { newScheme } nodes = { nodes } />
374
+ ) ) }
375
+ </ div >
376
+ </ div >
377
+ </ Resizable >
378
+ </ div >
379
+ </ >
380
+ ) }
381
+ </ div >
382
+ </ Dialog . Content >
383
+ </ Dialog >
384
+ </ >
385
+ ) ;
386
+ } ;
387
+ export default GraphViewModal ;
0 commit comments