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