Merge branch 'main' into i18n-france-arabic
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 { useSettingsStore } from '@/stores/settings'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
@@ -12,44 +12,8 @@ const GraphLabels = () => {
|
||||
const { t } = useTranslation()
|
||||
const label = useSettingsStore.use.queryLabel()
|
||||
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
|
||||
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])
|
||||
// Remove initial label fetch effect as it's now handled by fetchGraph based on lastSuccessfulQueryLabel
|
||||
|
||||
const getSearchEngine = useCallback(() => {
|
||||
// Create search engine
|
||||
@@ -93,40 +57,40 @@ const GraphLabels = () => {
|
||||
)
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
// Reset labels fetch status to allow fetching labels again
|
||||
// Reset fetch status flags
|
||||
useGraphStore.getState().setLabelsFetchAttempted(false)
|
||||
|
||||
// Reset graph data fetch status directly, not depending on allDatabaseLabels changes
|
||||
useGraphStore.getState().setGraphDataFetchAttempted(false)
|
||||
|
||||
// Fetch all labels again
|
||||
useGraphStore.getState().fetchAllDatabaseLabels()
|
||||
.then(() => {
|
||||
// Trigger a graph data reload by changing the query label back and forth
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
useSettingsStore.getState().setQueryLabel('')
|
||||
setTimeout(() => {
|
||||
useSettingsStore.getState().setQueryLabel(currentLabel)
|
||||
}, 0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to refresh labels:', error)
|
||||
})
|
||||
}, [])
|
||||
// Clear last successful query label to ensure labels are fetched
|
||||
useGraphStore.getState().setLastSuccessfulQueryLabel('')
|
||||
|
||||
// Get current label
|
||||
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('')
|
||||
setTimeout(() => {
|
||||
useSettingsStore.getState().setQueryLabel(currentLabel)
|
||||
}, 0)
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{rawGraph && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant={controlButtonVariant}
|
||||
onClick={handleRefresh}
|
||||
tooltip={t('graphPanel.graphLabels.refreshTooltip')}
|
||||
className="mr-1"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{/* Always show refresh button */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant={controlButtonVariant}
|
||||
onClick={handleRefresh}
|
||||
tooltip={t('graphPanel.graphLabels.refreshTooltip')}
|
||||
className="mr-1"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
<AsyncSelect<string>
|
||||
className="ml-2"
|
||||
triggerClassName="max-h-8"
|
||||
@@ -141,20 +105,23 @@ const GraphLabels = () => {
|
||||
placeholder={t('graphPanel.graphLabels.placeholder')}
|
||||
value={label !== null ? label : '*'}
|
||||
onChange={(newLabel) => {
|
||||
const currentLabel = useSettingsStore.getState().queryLabel
|
||||
const currentLabel = useSettingsStore.getState().queryLabel;
|
||||
|
||||
// select the last item means query all
|
||||
if (newLabel === '...') {
|
||||
newLabel = '*'
|
||||
newLabel = '*';
|
||||
}
|
||||
|
||||
// Handle reselecting the same label
|
||||
if (newLabel === currentLabel && newLabel !== '*') {
|
||||
newLabel = '*'
|
||||
newLabel = '*';
|
||||
}
|
||||
|
||||
// Update the label, which will trigger the useEffect to handle data loading
|
||||
useSettingsStore.getState().setQueryLabel(newLabel)
|
||||
// Reset graphDataFetchAttempted flag to ensure data fetch is triggered
|
||||
useGraphStore.getState().setGraphDataFetchAttempted(false);
|
||||
|
||||
// Update the label to trigger data loading
|
||||
useSettingsStore.getState().setQueryLabel(newLabel);
|
||||
}}
|
||||
clearable={false} // Prevent clearing value on reselect
|
||||
/>
|
||||
|
@@ -12,34 +12,52 @@ import { useSettingsStore } from '@/stores/settings'
|
||||
import seedrandom from 'seedrandom'
|
||||
|
||||
const validateGraph = (graph: RawGraph) => {
|
||||
// Check if graph exists
|
||||
if (!graph) {
|
||||
return false
|
||||
}
|
||||
if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
|
||||
return false
|
||||
console.log('Graph validation failed: graph is null');
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
const source = graph.getNode(edge.source)
|
||||
const target = graph.getNode(edge.target)
|
||||
const source = graph.getNode(edge.source);
|
||||
const target = graph.getNode(edge.target);
|
||||
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 = {
|
||||
@@ -53,16 +71,32 @@ export type NodeType = {
|
||||
export type EdgeType = { label: string }
|
||||
|
||||
const fetchGraph = async (label: string, maxDepth: number, minDegree: number) => {
|
||||
let rawData: any = null
|
||||
let rawData: any = null;
|
||||
|
||||
try {
|
||||
rawData = await queryGraphs(label, maxDepth, minDegree)
|
||||
} catch (e) {
|
||||
useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!')
|
||||
return 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 {
|
||||
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) {
|
||||
const nodeIdMap: Record<string, number> = {}
|
||||
@@ -192,6 +226,8 @@ const useLightrangeGraph = () => {
|
||||
// Use ref to track if data has been loaded and initial load
|
||||
const dataLoadedRef = useRef(false)
|
||||
const initialLoadRef = useRef(false)
|
||||
// Use ref to track if empty data has been handled
|
||||
const emptyDataHandledRef = useRef(false)
|
||||
|
||||
const getNode = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -224,11 +260,16 @@ const useLightrangeGraph = () => {
|
||||
|
||||
// Data fetching logic
|
||||
useEffect(() => {
|
||||
// Skip if fetch is already in progress or no query label
|
||||
if (fetchInProgressRef.current || !queryLabel) {
|
||||
// Skip if fetch is already in progress
|
||||
if (fetchInProgressRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Empty queryLabel should be only handle once(avoid infinite loop)
|
||||
if (!queryLabel && emptyDataHandledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only fetch data when graphDataFetchAttempted is false (avoids re-fetching on vite dev mode)
|
||||
if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) {
|
||||
// Set flags
|
||||
@@ -246,49 +287,106 @@ const useLightrangeGraph = () => {
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Fetching graph data...')
|
||||
console.log('Preparing graph data...')
|
||||
|
||||
// Use a local copy of the parameters
|
||||
const currentQueryLabel = queryLabel
|
||||
const currentMaxQueryDepth = maxQueryDepth
|
||||
const currentMinDegree = minDegree
|
||||
|
||||
// Fetch graph data
|
||||
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
|
||||
// Declare a variable to store data promise
|
||||
let dataPromise;
|
||||
|
||||
// 1. If query label is not empty, use fetchGraph
|
||||
if (currentQueryLabel) {
|
||||
dataPromise = fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree);
|
||||
} else {
|
||||
// 2. If query label is empty, set data to null
|
||||
console.log('Query label is empty, show empty graph')
|
||||
dataPromise = Promise.resolve(null);
|
||||
}
|
||||
|
||||
// 3. Process data
|
||||
dataPromise.then((data) => {
|
||||
const state = useGraphStore.getState()
|
||||
|
||||
// Reset state
|
||||
state.reset()
|
||||
|
||||
// Create and set new graph directly
|
||||
const newSigmaGraph = createSigmaGraph(data)
|
||||
data?.buildDynamicMap()
|
||||
// Check if data is empty or invalid
|
||||
if (!data || !data.nodes || data.nodes.length === 0) {
|
||||
// Create a graph with a single "Graph Is Empty" node
|
||||
const emptyGraph = new DirectedGraph();
|
||||
|
||||
// Set new graph data
|
||||
state.setSigmaGraph(newSigmaGraph)
|
||||
state.setRawGraph(data)
|
||||
// Add a single node with "Graph Is Empty" label
|
||||
emptyGraph.addNode('empty-graph-node', {
|
||||
label: t('graphPanel.emptyGraph'),
|
||||
color: '#cccccc', // gray color
|
||||
x: 0.5,
|
||||
y: 0.5,
|
||||
size: 15,
|
||||
borderColor: Constants.nodeBorderColor,
|
||||
borderSize: 0.2
|
||||
});
|
||||
|
||||
// Set graph to store
|
||||
state.setSigmaGraph(emptyGraph);
|
||||
state.setRawGraph(null);
|
||||
|
||||
// Still mark graph as empty for other logic
|
||||
state.setGraphIsEmpty(true);
|
||||
|
||||
// Only clear current label if it's not already empty
|
||||
if (currentQueryLabel) {
|
||||
useSettingsStore.getState().setQueryLabel('');
|
||||
}
|
||||
|
||||
// Clear last successful query label to ensure labels are fetched next time
|
||||
state.setLastSuccessfulQueryLabel('');
|
||||
|
||||
console.log('Graph data is empty, created graph with empty graph node');
|
||||
} 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
|
||||
dataLoadedRef.current = true
|
||||
initialLoadRef.current = true
|
||||
fetchInProgressRef.current = false
|
||||
|
||||
// Reset camera view
|
||||
state.setMoveToSelectedNode(true)
|
||||
|
||||
state.setIsFetching(false)
|
||||
|
||||
// Mark empty data as handled if data is empty and query label is empty
|
||||
if ((!data || !data.nodes || data.nodes.length === 0) && !currentQueryLabel) {
|
||||
emptyDataHandledRef.current = true;
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Error fetching graph data:', error)
|
||||
|
||||
// Reset state on error
|
||||
const state = useGraphStore.getState()
|
||||
state.setIsFetching(false)
|
||||
dataLoadedRef.current = false
|
||||
dataLoadedRef.current = false;
|
||||
fetchInProgressRef.current = false
|
||||
state.setGraphDataFetchAttempted(false)
|
||||
state.setLastSuccessfulQueryLabel('') // Clear last successful query label on error
|
||||
})
|
||||
}
|
||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching, t])
|
||||
|
||||
// Handle node expansion
|
||||
useEffect(() => {
|
||||
|
@@ -201,7 +201,8 @@
|
||||
"placeholder": "Search labels...",
|
||||
"andOthers": "And {count} others",
|
||||
"refreshTooltip": "Reload graph data"
|
||||
}
|
||||
},
|
||||
"emptyGraph": "Graph Is Empty"
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
|
@@ -198,7 +198,8 @@
|
||||
"placeholder": "搜索标签...",
|
||||
"andOthers": "还有 {count} 个",
|
||||
"refreshTooltip": "重新加载图形数据"
|
||||
}
|
||||
},
|
||||
"emptyGraph": "图谱数据为空"
|
||||
},
|
||||
"retrievePanel": {
|
||||
"chatMessage": {
|
||||
|
@@ -74,6 +74,8 @@ interface GraphState {
|
||||
|
||||
moveToSelectedNode: boolean
|
||||
isFetching: boolean
|
||||
graphIsEmpty: boolean
|
||||
lastSuccessfulQueryLabel: string
|
||||
|
||||
// Global flags to track data fetching attempts
|
||||
graphDataFetchAttempted: boolean
|
||||
@@ -88,6 +90,8 @@ interface GraphState {
|
||||
reset: () => void
|
||||
|
||||
setMoveToSelectedNode: (moveToSelectedNode: boolean) => void
|
||||
setGraphIsEmpty: (isEmpty: boolean) => void
|
||||
setLastSuccessfulQueryLabel: (label: string) => void
|
||||
|
||||
setRawGraph: (rawGraph: RawGraph | null) => void
|
||||
setSigmaGraph: (sigmaGraph: DirectedGraph | null) => void
|
||||
@@ -120,6 +124,8 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
|
||||
moveToSelectedNode: false,
|
||||
isFetching: false,
|
||||
graphIsEmpty: false,
|
||||
lastSuccessfulQueryLabel: '', // Initialize as empty to ensure fetchAllDatabaseLabels runs on first query
|
||||
|
||||
// Initialize global flags
|
||||
graphDataFetchAttempted: false,
|
||||
@@ -132,6 +138,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
|
||||
searchEngine: null,
|
||||
|
||||
setGraphIsEmpty: (isEmpty: boolean) => set({ graphIsEmpty: isEmpty }),
|
||||
setLastSuccessfulQueryLabel: (label: string) => set({ lastSuccessfulQueryLabel: label }),
|
||||
|
||||
|
||||
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||
@@ -155,7 +164,9 @@ const useGraphStoreBase = create<GraphState>()((set) => ({
|
||||
rawGraph: null,
|
||||
sigmaGraph: null, // to avoid other components from acccessing graph objects
|
||||
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