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
|
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 |
-
//
|
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
|
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 |
-
//
|
103 |
-
useGraphStore.getState().
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
|
|
|
|
|
|
116 |
|
117 |
return (
|
118 |
<div className="flex items-center">
|
119 |
-
{
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
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 |
-
//
|
157 |
-
|
|
|
|
|
|
|
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 |
-
|
|
|
17 |
}
|
|
|
|
|
18 |
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
|
|
|
22 |
for (const node of graph.nodes) {
|
23 |
if (!node.id || !node.labels || !node.properties) {
|
24 |
-
|
|
|
25 |
}
|
26 |
}
|
27 |
|
|
|
28 |
for (const edge of graph.edges) {
|
29 |
if (!edge.id || !edge.source || !edge.target) {
|
30 |
-
|
|
|
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 |
-
|
|
|
39 |
}
|
40 |
}
|
41 |
|
42 |
-
|
|
|
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 |
-
|
|
|
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 |
-
//
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|