yangdx commited on
Commit
f57fcd2
·
1 Parent(s): 1473fec

Fix: emtpy graph not display correctly after cleaning the database

Browse files

- Improved graph validation with detailed checks
- Added empty graph state handling
- Enhanced label fetching and refresh logic
- Tracked last successful query label
- Optimized data fetching flow

lightrag_webui/src/components/graph/GraphLabels.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef } from 'react'
2
  import { AsyncSelect } from '@/components/ui/AsyncSelect'
3
  import { useSettingsStore } from '@/stores/settings'
4
  import { useGraphStore } from '@/stores/graph'
@@ -12,44 +12,8 @@ const GraphLabels = () => {
12
  const { t } = useTranslation()
13
  const label = useSettingsStore.use.queryLabel()
14
  const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
15
- const rawGraph = useGraphStore.use.rawGraph()
16
- const labelsLoadedRef = useRef(false)
17
 
18
- // Track if a fetch is in progress to prevent multiple simultaneous fetches
19
- const fetchInProgressRef = useRef(false)
20
-
21
- // Fetch labels and trigger initial data load
22
- useEffect(() => {
23
- // Check if we've already attempted to fetch labels in this session
24
- const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
25
-
26
- // Only fetch if we haven't attempted in this session and no fetch is in progress
27
- if (!labelsFetchAttempted && !fetchInProgressRef.current) {
28
- fetchInProgressRef.current = true
29
- // Set global flag to indicate we've attempted to fetch in this session
30
- useGraphStore.getState().setLabelsFetchAttempted(true)
31
-
32
- useGraphStore.getState().fetchAllDatabaseLabels()
33
- .then(() => {
34
- labelsLoadedRef.current = true
35
- fetchInProgressRef.current = false
36
- })
37
- .catch((error) => {
38
- console.error('Failed to fetch labels:', error)
39
- fetchInProgressRef.current = false
40
- // Reset global flag to allow retry
41
- useGraphStore.getState().setLabelsFetchAttempted(false)
42
- })
43
- }
44
- }, []) // Empty dependency array ensures this only runs once on mount
45
-
46
- // Trigger data load when labels are loaded
47
- useEffect(() => {
48
- if (labelsLoadedRef.current) {
49
- // Reset the fetch attempted flag to force a new data fetch
50
- useGraphStore.getState().setGraphDataFetchAttempted(false)
51
- }
52
- }, [label])
53
 
54
  const getSearchEngine = useCallback(() => {
55
  // Create search engine
@@ -93,40 +57,40 @@ const GraphLabels = () => {
93
  )
94
 
95
  const handleRefresh = useCallback(() => {
96
- // Reset labels fetch status to allow fetching labels again
97
  useGraphStore.getState().setLabelsFetchAttempted(false)
98
-
99
- // Reset graph data fetch status directly, not depending on allDatabaseLabels changes
100
  useGraphStore.getState().setGraphDataFetchAttempted(false)
101
-
102
- // Fetch all labels again
103
- useGraphStore.getState().fetchAllDatabaseLabels()
104
- .then(() => {
105
- // Trigger a graph data reload by changing the query label back and forth
106
- const currentLabel = useSettingsStore.getState().queryLabel
107
- useSettingsStore.getState().setQueryLabel('')
108
- setTimeout(() => {
109
- useSettingsStore.getState().setQueryLabel(currentLabel)
110
- }, 0)
111
- })
112
- .catch((error) => {
113
- console.error('Failed to refresh labels:', error)
114
- })
115
- }, [])
 
 
 
116
 
117
  return (
118
  <div className="flex items-center">
119
- {rawGraph && (
120
- <Button
121
- size="icon"
122
- variant={controlButtonVariant}
123
- onClick={handleRefresh}
124
- tooltip={t('graphPanel.graphLabels.refreshTooltip')}
125
- className="mr-1"
126
- >
127
- <RefreshCw className="h-4 w-4" />
128
- </Button>
129
- )}
130
  <AsyncSelect<string>
131
  className="ml-2"
132
  triggerClassName="max-h-8"
@@ -141,20 +105,23 @@ const GraphLabels = () => {
141
  placeholder={t('graphPanel.graphLabels.placeholder')}
142
  value={label !== null ? label : '*'}
143
  onChange={(newLabel) => {
144
- const currentLabel = useSettingsStore.getState().queryLabel
145
 
146
  // select the last item means query all
147
  if (newLabel === '...') {
148
- newLabel = '*'
149
  }
150
 
151
  // Handle reselecting the same label
152
  if (newLabel === currentLabel && newLabel !== '*') {
153
- newLabel = '*'
154
  }
155
 
156
- // Update the label, which will trigger the useEffect to handle data loading
157
- useSettingsStore.getState().setQueryLabel(newLabel)
 
 
 
158
  }}
159
  clearable={false} // Prevent clearing value on reselect
160
  />
 
1
+ import { useCallback } from 'react'
2
  import { AsyncSelect } from '@/components/ui/AsyncSelect'
3
  import { useSettingsStore } from '@/stores/settings'
4
  import { useGraphStore } from '@/stores/graph'
 
12
  const { t } = useTranslation()
13
  const label = useSettingsStore.use.queryLabel()
14
  const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
 
 
15
 
16
+ // Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  const getSearchEngine = useCallback(() => {
19
  // Create search engine
 
57
  )
58
 
59
  const handleRefresh = useCallback(() => {
60
+ // Reset fetch status flags
61
  useGraphStore.getState().setLabelsFetchAttempted(false)
 
 
62
  useGraphStore.getState().setGraphDataFetchAttempted(false)
63
+
64
+ // Clear last successful query label to ensure labels are fetched
65
+ useGraphStore.getState().setLastSuccessfulQueryLabel('')
66
+
67
+ // Get current label
68
+ const currentLabel = useSettingsStore.getState().queryLabel
69
+
70
+ // If current label is empty, use default label '*'
71
+ if (!currentLabel) {
72
+ useSettingsStore.getState().setQueryLabel('*')
73
+ } else {
74
+ // Trigger data reload
75
+ useSettingsStore.getState().setQueryLabel('')
76
+ setTimeout(() => {
77
+ useSettingsStore.getState().setQueryLabel(currentLabel)
78
+ }, 0)
79
+ }
80
+ }, []);
81
 
82
  return (
83
  <div className="flex items-center">
84
+ {/* Always show refresh button */}
85
+ <Button
86
+ size="icon"
87
+ variant={controlButtonVariant}
88
+ onClick={handleRefresh}
89
+ tooltip={t('graphPanel.graphLabels.refreshTooltip')}
90
+ className="mr-1"
91
+ >
92
+ <RefreshCw className="h-4 w-4" />
93
+ </Button>
 
94
  <AsyncSelect<string>
95
  className="ml-2"
96
  triggerClassName="max-h-8"
 
105
  placeholder={t('graphPanel.graphLabels.placeholder')}
106
  value={label !== null ? label : '*'}
107
  onChange={(newLabel) => {
108
+ const currentLabel = useSettingsStore.getState().queryLabel;
109
 
110
  // select the last item means query all
111
  if (newLabel === '...') {
112
+ newLabel = '*';
113
  }
114
 
115
  // Handle reselecting the same label
116
  if (newLabel === currentLabel && newLabel !== '*') {
117
+ newLabel = '*';
118
  }
119
 
120
+ // Reset graphDataFetchAttempted flag to ensure data fetch is triggered
121
+ useGraphStore.getState().setGraphDataFetchAttempted(false);
122
+
123
+ // Update the label to trigger data loading
124
+ useSettingsStore.getState().setQueryLabel(newLabel);
125
  }}
126
  clearable={false} // Prevent clearing value on reselect
127
  />
lightrag_webui/src/hooks/useLightragGraph.tsx CHANGED
@@ -12,34 +12,52 @@ import { useSettingsStore } from '@/stores/settings'
12
  import seedrandom from 'seedrandom'
13
 
14
  const validateGraph = (graph: RawGraph) => {
 
15
  if (!graph) {
16
- return false
 
17
  }
 
 
18
  if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
19
- return false
 
 
 
 
 
 
 
20
  }
21
 
 
22
  for (const node of graph.nodes) {
23
  if (!node.id || !node.labels || !node.properties) {
24
- return false
 
25
  }
26
  }
27
 
 
28
  for (const edge of graph.edges) {
29
  if (!edge.id || !edge.source || !edge.target) {
30
- return false
 
31
  }
32
  }
33
 
 
34
  for (const edge of graph.edges) {
35
- const source = graph.getNode(edge.source)
36
- const target = graph.getNode(edge.target)
37
  if (source == undefined || target == undefined) {
38
- return false
 
39
  }
40
  }
41
 
42
- return true
 
43
  }
44
 
45
  export type NodeType = {
@@ -53,16 +71,32 @@ export type NodeType = {
53
  export type EdgeType = { label: string }
54
 
55
  const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
56
- let rawData: any = null
 
 
 
 
 
 
 
 
 
 
 
 
57
 
 
 
 
58
  try {
59
- rawData = await queryGraphs(label, maxDepth, minDegree)
 
60
  } catch (e) {
61
- useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
62
- return null
63
  }
64
 
65
- let rawGraph = null
66
 
67
  if (rawData) {
68
  const nodeIdMap: Record<string, number> = {}
@@ -260,22 +294,48 @@ const useLightrangeGraph = () => {
260
  // Reset state
261
  state.reset()
262
 
263
- // Create and set new graph directly
264
- const newSigmaGraph = createSigmaGraph(data)
265
- data?.buildDynamicMap()
266
-
267
- // Set new graph data
268
- state.setSigmaGraph(newSigmaGraph)
269
- state.setRawGraph(data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  // Update flags
272
  dataLoadedRef.current = true
273
  initialLoadRef.current = true
274
  fetchInProgressRef.current = false
275
-
276
- // Reset camera view
277
- state.setMoveToSelectedNode(true)
278
-
279
  state.setIsFetching(false)
280
  }).catch((error) => {
281
  console.error('Error fetching graph data:', error)
@@ -283,9 +343,10 @@ const useLightrangeGraph = () => {
283
  // Reset state on error
284
  const state = useGraphStore.getState()
285
  state.setIsFetching(false)
286
- dataLoadedRef.current = false
287
  fetchInProgressRef.current = false
288
  state.setGraphDataFetchAttempted(false)
 
289
  })
290
  }
291
  }, [queryLabel, maxQueryDepth, minDegree, isFetching])
 
12
  import seedrandom from 'seedrandom'
13
 
14
  const validateGraph = (graph: RawGraph) => {
15
+ // Check if graph exists
16
  if (!graph) {
17
+ console.log('Graph validation failed: graph is null');
18
+ return false;
19
  }
20
+
21
+ // Check if nodes and edges are arrays
22
  if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
23
+ console.log('Graph validation failed: nodes or edges is not an array');
24
+ return false;
25
+ }
26
+
27
+ // Check if nodes array is empty
28
+ if (graph.nodes.length === 0) {
29
+ console.log('Graph validation failed: nodes array is empty');
30
+ return false;
31
  }
32
 
33
+ // Validate each node
34
  for (const node of graph.nodes) {
35
  if (!node.id || !node.labels || !node.properties) {
36
+ console.log('Graph validation failed: invalid node structure');
37
+ return false;
38
  }
39
  }
40
 
41
+ // Validate each edge
42
  for (const edge of graph.edges) {
43
  if (!edge.id || !edge.source || !edge.target) {
44
+ console.log('Graph validation failed: invalid edge structure');
45
+ return false;
46
  }
47
  }
48
 
49
+ // Validate edge connections
50
  for (const edge of graph.edges) {
51
+ const source = graph.getNode(edge.source);
52
+ const target = graph.getNode(edge.target);
53
  if (source == undefined || target == undefined) {
54
+ console.log('Graph validation failed: edge references non-existent node');
55
+ return false;
56
  }
57
  }
58
 
59
+ console.log('Graph validation passed');
60
+ return true;
61
  }
62
 
63
  export type NodeType = {
 
71
  export type EdgeType = { label: string }
72
 
73
  const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
74
+ let rawData: any = null;
75
+
76
+ // Check if we need to fetch all database labels first
77
+ const lastSuccessfulQueryLabel = useGraphStore.getState().lastSuccessfulQueryLabel;
78
+ if (!lastSuccessfulQueryLabel) {
79
+ console.log('Last successful query label is empty, fetching all database labels first...');
80
+ try {
81
+ await useGraphStore.getState().fetchAllDatabaseLabels();
82
+ } catch (e) {
83
+ console.error('Failed to fetch all database labels:', e);
84
+ // Continue with graph fetch even if labels fetch fails
85
+ }
86
+ }
87
 
88
+ // If label is empty, use default label '*'
89
+ const queryLabel = label || '*';
90
+
91
  try {
92
+ console.log(`Fetching graph data with label: ${queryLabel}, maxDepth: ${maxDepth}, minDegree: ${minDegree}`);
93
+ rawData = await queryGraphs(queryLabel, maxDepth, minDegree);
94
  } catch (e) {
95
+ useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!');
96
+ return null;
97
  }
98
 
99
+ let rawGraph = null;
100
 
101
  if (rawData) {
102
  const nodeIdMap: Record<string, number> = {}
 
294
  // Reset state
295
  state.reset()
296
 
297
+ // Check if data is empty or invalid
298
+ if (!data || !data.nodes || data.nodes.length === 0) {
299
+ // Create an empty graph
300
+ const emptyGraph = new DirectedGraph();
301
+
302
+ // Set empty graph to store
303
+ state.setSigmaGraph(emptyGraph);
304
+ state.setRawGraph(null);
305
+
306
+ // Show "Graph Is Empty" placeholder
307
+ state.setGraphIsEmpty(true);
308
+
309
+ // Clear current label
310
+ useSettingsStore.getState().setQueryLabel('');
311
+
312
+ // Clear last successful query label to ensure labels are fetched next time
313
+ state.setLastSuccessfulQueryLabel('');
314
+
315
+ console.log('Graph data is empty or invalid, set empty graph');
316
+ } else {
317
+ // Create and set new graph
318
+ const newSigmaGraph = createSigmaGraph(data);
319
+ data.buildDynamicMap();
320
+
321
+ // Set new graph data
322
+ state.setSigmaGraph(newSigmaGraph);
323
+ state.setRawGraph(data);
324
+ state.setGraphIsEmpty(false);
325
+
326
+ // Update last successful query label
327
+ state.setLastSuccessfulQueryLabel(currentQueryLabel);
328
+
329
+ // Reset camera view
330
+ state.setMoveToSelectedNode(true);
331
+
332
+ console.log('Graph data loaded successfully');
333
+ }
334
 
335
  // Update flags
336
  dataLoadedRef.current = true
337
  initialLoadRef.current = true
338
  fetchInProgressRef.current = false
 
 
 
 
339
  state.setIsFetching(false)
340
  }).catch((error) => {
341
  console.error('Error fetching graph data:', error)
 
343
  // Reset state on error
344
  const state = useGraphStore.getState()
345
  state.setIsFetching(false)
346
+ dataLoadedRef.current = false;
347
  fetchInProgressRef.current = false
348
  state.setGraphDataFetchAttempted(false)
349
+ state.setLastSuccessfulQueryLabel('') // Clear last successful query label on error
350
  })
351
  }
352
  }, [queryLabel, maxQueryDepth, minDegree, isFetching])
lightrag_webui/src/stores/graph.ts CHANGED
@@ -74,6 +74,8 @@ interface GraphState {
74
 
75
  moveToSelectedNode: boolean
76
  isFetching: boolean
 
 
77
 
78
  // Global flags to track data fetching attempts
79
  graphDataFetchAttempted: boolean
@@ -88,6 +90,8 @@ interface GraphState {
88
  reset: () => void
89
 
90
  setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
 
 
91
 
92
  setRawGraph: (rawGraph: RawGraph | null) => void
93
  setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
@@ -120,6 +124,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
120
 
121
  moveToSelectedNode: false,
122
  isFetching: false,
 
 
123
 
124
  // Initialize global flags
125
  graphDataFetchAttempted: false,
@@ -132,6 +138,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
132
 
133
  searchEngine: null,
134
 
 
 
 
135
 
136
  setIsFetching: (isFetching: boolean) => set({ isFetching }),
137
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
@@ -155,7 +164,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
155
  rawGraph: null,
156
  sigmaGraph: null, // to avoid other components from acccessing graph objects
157
  searchEngine: null,
158
- moveToSelectedNode: false
 
 
159
  });
160
  },
161
 
 
74
 
75
  moveToSelectedNode: boolean
76
  isFetching: boolean
77
+ graphIsEmpty: boolean
78
+ lastSuccessfulQueryLabel: string
79
 
80
  // Global flags to track data fetching attempts
81
  graphDataFetchAttempted: boolean
 
90
  reset: () => void
91
 
92
  setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
93
+ setGraphIsEmpty: (isEmpty: boolean) => void
94
+ setLastSuccessfulQueryLabel: (label: string) => void
95
 
96
  setRawGraph: (rawGraph: RawGraph | null) => void
97
  setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
 
124
 
125
  moveToSelectedNode: false,
126
  isFetching: false,
127
+ graphIsEmpty: false,
128
+ lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query
129
 
130
  // Initialize global flags
131
  graphDataFetchAttempted: false,
 
138
 
139
  searchEngine: null,
140
 
141
+ setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
142
+ setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }),
143
+
144
 
145
  setIsFetching: (isFetching: boolean) => set({ isFetching }),
146
  setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
 
164
  rawGraph: null,
165
  sigmaGraph: null, // to avoid other components from acccessing graph objects
166
  searchEngine: null,
167
+ moveToSelectedNode: false,
168
+ graphIsEmpty: false
169
+ // Do not reset lastSuccessfulQueryLabel here as it's used to track query history
170
  });
171
  },
172