Fix: emtpy graph not display correctly after cleaning the database

- 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
This commit is contained in:
yangdx
2025-03-21 19:56:45 +08:00
parent 58f8c76ef9
commit 35e00f1eb2
3 changed files with 141 additions and 102 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react' import { useCallback } from 'react'
import { AsyncSelect } from '@/components/ui/AsyncSelect' import { AsyncSelect } from '@/components/ui/AsyncSelect'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import { useGraphStore } from '@/stores/graph' import { useGraphStore } from '@/stores/graph'
@@ -12,44 +12,8 @@ const GraphLabels = () => {
const { t } = useTranslation() const { t } = useTranslation()
const label = useSettingsStore.use.queryLabel() const label = useSettingsStore.use.queryLabel()
const allDatabaseLabels = useGraphStore.use.allDatabaseLabels() const allDatabaseLabels = useGraphStore.use.allDatabaseLabels()
const rawGraph = useGraphStore.use.rawGraph()
const labelsLoadedRef = useRef(false)
// Track if a fetch is in progress to prevent multiple simultaneous fetches // Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel
const fetchInProgressRef = useRef(false)
// Fetch labels and trigger initial data load
useEffect(() => {
// Check if we've already attempted to fetch labels in this session
const labelsFetchAttempted = useGraphStore.getState().labelsFetchAttempted
// Only fetch if we haven't attempted in this session and no fetch is in progress
if (!labelsFetchAttempted && !fetchInProgressRef.current) {
fetchInProgressRef.current = true
// Set global flag to indicate we've attempted to fetch in this session
useGraphStore.getState().setLabelsFetchAttempted(true)
useGraphStore.getState().fetchAllDatabaseLabels()
.then(() => {
labelsLoadedRef.current = true
fetchInProgressRef.current = false
})
.catch((error) => {
console.error('Failed to fetch labels:', error)
fetchInProgressRef.current = false
// Reset global flag to allow retry
useGraphStore.getState().setLabelsFetchAttempted(false)
})
}
}, []) // Empty dependency array ensures this only runs once on mount
// Trigger data load when labels are loaded
useEffect(() => {
if (labelsLoadedRef.current) {
// Reset the fetch attempted flag to force a new data fetch
useGraphStore.getState().setGraphDataFetchAttempted(false)
}
}, [label])
const getSearchEngine = useCallback(() => { const getSearchEngine = useCallback(() => {
// Create search engine // Create search engine
@@ -93,40 +57,40 @@ const GraphLabels = () => {
) )
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
// Reset labels fetch status to allow fetching labels again // Reset fetch status flags
useGraphStore.getState().setLabelsFetchAttempted(false) useGraphStore.getState().setLabelsFetchAttempted(false)
// Reset graph data fetch status directly, not depending on allDatabaseLabels changes
useGraphStore.getState().setGraphDataFetchAttempted(false) useGraphStore.getState().setGraphDataFetchAttempted(false)
// Fetch all labels again // Clear last successful query label to ensure labels are fetched
useGraphStore.getState().fetchAllDatabaseLabels() useGraphStore.getState().setLastSuccessfulQueryLabel('')
.then(() => {
// Trigger a graph data reload by changing the query label back and forth // Get current label
const currentLabel = useSettingsStore.getState().queryLabel const currentLabel = useSettingsStore.getState().queryLabel
useSettingsStore.getState().setQueryLabel('')
setTimeout(() => { // If current label is empty, use default label '*'
useSettingsStore.getState().setQueryLabel(currentLabel) if (!currentLabel) {
}, 0) useSettingsStore.getState().setQueryLabel('*')
}) } else {
.catch((error) => { // Trigger data reload
console.error('Failed to refresh labels:', error) useSettingsStore.getState().setQueryLabel('')
}) setTimeout(() => {
}, []) useSettingsStore.getState().setQueryLabel(currentLabel)
}, 0)
}
}, []);
return ( return (
<div className="flex items-center"> <div className="flex items-center">
{rawGraph && ( {/* Always show refresh button */}
<Button <Button
size="icon" size="icon"
variant={controlButtonVariant} variant={controlButtonVariant}
onClick={handleRefresh} onClick={handleRefresh}
tooltip={t('graphPanel.graphLabels.refreshTooltip')} tooltip={t('graphPanel.graphLabels.refreshTooltip')}
className="mr-1" className="mr-1"
> >
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
</Button> </Button>
)}
<AsyncSelect<string> <AsyncSelect<string>
className="ml-2" className="ml-2"
triggerClassName="max-h-8" triggerClassName="max-h-8"
@@ -141,20 +105,23 @@ const GraphLabels = () => {
placeholder={t('graphPanel.graphLabels.placeholder')} placeholder={t('graphPanel.graphLabels.placeholder')}
value={label !== null ? label : '*'} value={label !== null ? label : '*'}
onChange={(newLabel) => { onChange={(newLabel) => {
const currentLabel = useSettingsStore.getState().queryLabel const currentLabel = useSettingsStore.getState().queryLabel;
// select the last item means query all // select the last item means query all
if (newLabel === '...') { if (newLabel === '...') {
newLabel = '*' newLabel = '*';
} }
// Handle reselecting the same label // Handle reselecting the same label
if (newLabel === currentLabel && newLabel !== '*') { if (newLabel === currentLabel && newLabel !== '*') {
newLabel = '*' newLabel = '*';
} }
// Update the label, which will trigger the useEffect to handle data loading // Reset graphDataFetchAttempted flag to ensure data fetch is triggered
useSettingsStore.getState().setQueryLabel(newLabel) useGraphStore.getState().setGraphDataFetchAttempted(false);
// Update the label to trigger data loading
useSettingsStore.getState().setQueryLabel(newLabel);
}} }}
clearable={false} // Prevent clearing value on reselect clearable={false} // Prevent clearing value on reselect
/> />

View File

@@ -12,34 +12,52 @@ import { useSettingsStore } from '@/stores/settings'
import seedrandom from 'seedrandom' import seedrandom from 'seedrandom'
const validateGraph = (graph: RawGraph) => { const validateGraph = (graph: RawGraph) => {
// Check if graph exists
if (!graph) { if (!graph) {
return false console.log('Graph validation failed: graph is null');
} return false;
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
return false
} }
// Check if nodes and edges are arrays
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
console.log('Graph validation failed: nodes or edges is not an array');
return false;
}
// Check if nodes array is empty
if (graph.nodes.length === 0) {
console.log('Graph validation failed: nodes array is empty');
return false;
}
// Validate each node
for (const node of graph.nodes) { for (const node of graph.nodes) {
if (!node.id || !node.labels || !node.properties) { if (!node.id || !node.labels || !node.properties) {
return false console.log('Graph validation failed: invalid node structure');
return false;
} }
} }
// Validate each edge
for (const edge of graph.edges) { for (const edge of graph.edges) {
if (!edge.id || !edge.source || !edge.target) { if (!edge.id || !edge.source || !edge.target) {
return false console.log('Graph validation failed: invalid edge structure');
return false;
} }
} }
// Validate edge connections
for (const edge of graph.edges) { for (const edge of graph.edges) {
const source = graph.getNode(edge.source) const source = graph.getNode(edge.source);
const target = graph.getNode(edge.target) const target = graph.getNode(edge.target);
if (source == undefined || target == undefined) { if (source == undefined || target == undefined) {
return false console.log('Graph validation failed: edge references non-existent node');
return false;
} }
} }
return true console.log('Graph validation passed');
return true;
} }
export type NodeType = { export type NodeType = {
@@ -53,16 +71,32 @@ export type NodeType = {
export type EdgeType = { label: string } export type EdgeType = { label: string }
const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => { const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
let rawData: any = null let rawData: any = null;
try { // Check if we need to fetch all database labels first
rawData = await queryGraphs(label, maxDepth, minDegree) const lastSuccessfulQueryLabel = useGraphStore.getState().lastSuccessfulQueryLabel;
} catch (e) { if (!lastSuccessfulQueryLabel) {
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!') console.log('Last successful query label is empty, fetching all database labels first...');
return null try {
await useGraphStore.getState().fetchAllDatabaseLabels();
} catch (e) {
console.error('Failed to fetch all database labels:', e);
// Continue with graph fetch even if labels fetch fails
}
} }
let rawGraph = null // If label is empty, use default label '*'
const queryLabel = label || '*';
try {
console.log(`Fetching graph data with label: ${queryLabel}, maxDepth: ${maxDepth}, minDegree: ${minDegree}`);
rawData = await queryGraphs(queryLabel, maxDepth, minDegree);
} catch (e) {
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!');
return null;
}
let rawGraph = null;
if (rawData) { if (rawData) {
const nodeIdMap: Record<string, number> = {} const nodeIdMap: Record<string, number> = {}
@@ -260,22 +294,48 @@ const useLightrangeGraph = () => {
// Reset state // Reset state
state.reset() state.reset()
// Create and set new graph directly // Check if data is empty or invalid
const newSigmaGraph = createSigmaGraph(data) if (!data || !data.nodes || data.nodes.length === 0) {
data?.buildDynamicMap() // Create an empty graph
const emptyGraph = new DirectedGraph();
// Set new graph data // Set empty graph to store
state.setSigmaGraph(newSigmaGraph) state.setSigmaGraph(emptyGraph);
state.setRawGraph(data) state.setRawGraph(null);
// Show "Graph Is Empty" placeholder
state.setGraphIsEmpty(true);
// Clear current label
useSettingsStore.getState().setQueryLabel('');
// Clear last successful query label to ensure labels are fetched next time
state.setLastSuccessfulQueryLabel('');
console.log('Graph data is empty or invalid, set empty graph');
} else {
// Create and set new graph
const newSigmaGraph = createSigmaGraph(data);
data.buildDynamicMap();
// Set new graph data
state.setSigmaGraph(newSigmaGraph);
state.setRawGraph(data);
state.setGraphIsEmpty(false);
// Update last successful query label
state.setLastSuccessfulQueryLabel(currentQueryLabel);
// Reset camera view
state.setMoveToSelectedNode(true);
console.log('Graph data loaded successfully');
}
// Update flags // Update flags
dataLoadedRef.current = true dataLoadedRef.current = true
initialLoadRef.current = true initialLoadRef.current = true
fetchInProgressRef.current = false fetchInProgressRef.current = false
// Reset camera view
state.setMoveToSelectedNode(true)
state.setIsFetching(false) state.setIsFetching(false)
}).catch((error) => { }).catch((error) => {
console.error('Error fetching graph data:', error) console.error('Error fetching graph data:', error)
@@ -283,9 +343,10 @@ const useLightrangeGraph = () => {
// Reset state on error // Reset state on error
const state = useGraphStore.getState() const state = useGraphStore.getState()
state.setIsFetching(false) state.setIsFetching(false)
dataLoadedRef.current = false dataLoadedRef.current = false;
fetchInProgressRef.current = false fetchInProgressRef.current = false
state.setGraphDataFetchAttempted(false) state.setGraphDataFetchAttempted(false)
state.setLastSuccessfulQueryLabel('') // Clear last successful query label on error
}) })
} }
}, [queryLabel, maxQueryDepth, minDegree, isFetching]) }, [queryLabel, maxQueryDepth, minDegree, isFetching])

View File

@@ -74,6 +74,8 @@ interface GraphState {
moveToSelectedNode: boolean moveToSelectedNode: boolean
isFetching: boolean isFetching: boolean
graphIsEmpty: boolean
lastSuccessfulQueryLabel: string
// Global flags to track data fetching attempts // Global flags to track data fetching attempts
graphDataFetchAttempted: boolean graphDataFetchAttempted: boolean
@@ -88,6 +90,8 @@ interface GraphState {
reset: () => void reset: () => void
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
setGraphIsEmpty: (isEmpty: boolean) => void
setLastSuccessfulQueryLabel: (label: string) => void
setRawGraph: (rawGraph: RawGraph | null) => void setRawGraph: (rawGraph: RawGraph | null) => void
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
@@ -120,6 +124,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
moveToSelectedNode: false, moveToSelectedNode: false,
isFetching: false, isFetching: false,
graphIsEmpty: false,
lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query
// Initialize global flags // Initialize global flags
graphDataFetchAttempted: false, graphDataFetchAttempted: false,
@@ -132,6 +138,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
searchEngine: null, searchEngine: null,
setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }),
setIsFetching: (isFetching: boolean) => set({ isFetching }), setIsFetching: (isFetching: boolean) => set({ isFetching }),
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
@@ -155,7 +164,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
rawGraph: null, rawGraph: null,
sigmaGraph: null, // to avoid other components from acccessing graph objects sigmaGraph: null, // to avoid other components from acccessing graph objects
searchEngine: null, searchEngine: null,
moveToSelectedNode: false moveToSelectedNode: false,
graphIsEmpty: false
// Do not reset lastSuccessfulQueryLabel here as it's used to track query history
}); });
}, },