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:
@@ -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,30 +57,31 @@ 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
|
||||||
|
|
||||||
|
// If current label is empty, use default label '*'
|
||||||
|
if (!currentLabel) {
|
||||||
|
useSettingsStore.getState().setQueryLabel('*')
|
||||||
|
} else {
|
||||||
|
// Trigger data reload
|
||||||
useSettingsStore.getState().setQueryLabel('')
|
useSettingsStore.getState().setQueryLabel('')
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
useSettingsStore.getState().setQueryLabel(currentLabel)
|
useSettingsStore.getState().setQueryLabel(currentLabel)
|
||||||
}, 0)
|
}, 0)
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
}, []);
|
||||||
console.error('Failed to refresh labels:', error)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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}
|
||||||
@@ -126,7 +91,6 @@ const GraphLabels = () => {
|
|||||||
>
|
>
|
||||||
<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
|
||||||
/>
|
/>
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
// Check if we need to fetch all database labels first
|
||||||
|
const lastSuccessfulQueryLabel = useGraphStore.getState().lastSuccessfulQueryLabel;
|
||||||
|
if (!lastSuccessfulQueryLabel) {
|
||||||
|
console.log('Last successful query label is empty, fetching all database labels first...');
|
||||||
try {
|
try {
|
||||||
rawData = await queryGraphs(label, maxDepth, minDegree)
|
await useGraphStore.getState().fetchAllDatabaseLabels();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
console.error('Failed to fetch all database labels:', e);
|
||||||
return null
|
// 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 empty graph to store
|
||||||
|
state.setSigmaGraph(emptyGraph);
|
||||||
|
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
|
// Set new graph data
|
||||||
state.setSigmaGraph(newSigmaGraph)
|
state.setSigmaGraph(newSigmaGraph);
|
||||||
state.setRawGraph(data)
|
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])
|
||||||
|
@@ -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
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user