Minimized API request between Tab view change
This commit is contained in:
@@ -75,8 +75,8 @@ class LightragPathFilter(logging.Filter):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# Define paths to be filtered
|
# Define paths to be filtered
|
||||||
# self.filtered_paths = ["/documents", "/health", "/webui/"]
|
self.filtered_paths = ["/documents", "/health", "/webui/"]
|
||||||
self.filtered_paths = ["/health", "/webui/"]
|
# self.filtered_paths = ["/health", "/webui/"]
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
try:
|
try:
|
||||||
|
@@ -22,7 +22,7 @@ import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
|||||||
function App() {
|
function App() {
|
||||||
const message = useBackendState.use.message()
|
const message = useBackendState.use.message()
|
||||||
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||||
const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback, useEffect, useRef } 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'
|
||||||
@@ -10,6 +10,37 @@ 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 labelsLoadedRef = useRef(false)
|
||||||
|
|
||||||
|
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
||||||
|
const fetchInProgressRef = useRef(false)
|
||||||
|
|
||||||
|
// Fetch labels once on component mount, using global flag to prevent duplicates
|
||||||
|
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)
|
||||||
|
|
||||||
|
console.log('Fetching graph labels (once per session)...')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
const getSearchEngine = useCallback(() => {
|
const getSearchEngine = useCallback(() => {
|
||||||
// Create search engine
|
// Create search engine
|
||||||
|
@@ -25,12 +25,10 @@ const TabContent: React.FC<TabContentProps> = ({ tabId, children, className = ''
|
|||||||
};
|
};
|
||||||
}, [tabId, setTabVisibility]);
|
}, [tabId, setTabVisibility]);
|
||||||
|
|
||||||
if (!isVisible) {
|
// Use CSS to hide content instead of not rendering it
|
||||||
return null;
|
// This prevents components from unmounting when tabs are switched
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={`${className} ${isVisible ? '' : 'hidden'}`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { TabVisibilityContext } from './context';
|
import { TabVisibilityContext } from './context';
|
||||||
import { TabVisibilityContextType } from './types';
|
import { TabVisibilityContextType } from './types';
|
||||||
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
|
|
||||||
interface TabVisibilityProviderProps {
|
interface TabVisibilityProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,7 +12,21 @@ interface TabVisibilityProviderProps {
|
|||||||
* Manages the visibility state of tabs throughout the application
|
* Manages the visibility state of tabs throughout the application
|
||||||
*/
|
*/
|
||||||
export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
|
export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ children }) => {
|
||||||
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>({});
|
// Get current tab from settings store
|
||||||
|
const currentTab = useSettingsStore.use.currentTab();
|
||||||
|
|
||||||
|
// Initialize visibility state with current tab as visible
|
||||||
|
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
|
||||||
|
[currentTab]: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update visibility when current tab changes
|
||||||
|
useEffect(() => {
|
||||||
|
setVisibleTabs((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[currentTab]: true
|
||||||
|
}));
|
||||||
|
}, [currentTab]);
|
||||||
|
|
||||||
// Create the context value with memoization to prevent unnecessary re-renders
|
// Create the context value with memoization to prevent unnecessary re-renders
|
||||||
const contextValue = useMemo<TabVisibilityContextType>(
|
const contextValue = useMemo<TabVisibilityContextType>(
|
||||||
|
@@ -1,5 +1,38 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
import { backendBaseUrl } from '@/lib/constants'
|
import { backendBaseUrl } from '@/lib/constants'
|
||||||
|
|
||||||
export default function ApiSite() {
|
export default function ApiSite() {
|
||||||
return <iframe src={backendBaseUrl + '/docs'} className="size-full" />
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isApiTabVisible = isTabVisible('api')
|
||||||
|
const [iframeLoaded, setIframeLoaded] = useState(false)
|
||||||
|
|
||||||
|
// Load the iframe once on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (!iframeLoaded) {
|
||||||
|
setIframeLoaded(true)
|
||||||
|
}
|
||||||
|
}, [iframeLoaded])
|
||||||
|
|
||||||
|
// Use CSS to hide content when tab is not visible
|
||||||
|
return (
|
||||||
|
<div className={`size-full ${isApiTabVisible ? '' : 'hidden'}`}>
|
||||||
|
{iframeLoaded ? (
|
||||||
|
<iframe
|
||||||
|
src={backendBaseUrl + '/docs'}
|
||||||
|
className="size-full w-full h-full"
|
||||||
|
style={{ width: '100%', height: '100%', border: 'none' }}
|
||||||
|
// Use key to ensure iframe doesn't reload
|
||||||
|
key="api-docs-iframe"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-background">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||||
|
<p>Loading API Documentation...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -26,6 +27,9 @@ export default function DocumentManager() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const health = useBackendState.use.health()
|
const health = useBackendState.use.health()
|
||||||
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
|
||||||
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isDocumentsTabVisible = isTabVisible('documents')
|
||||||
|
const initialLoadRef = useRef(false)
|
||||||
|
|
||||||
const fetchDocuments = useCallback(async () => {
|
const fetchDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -50,9 +54,13 @@ export default function DocumentManager() {
|
|||||||
}
|
}
|
||||||
}, [setDocs, t])
|
}, [setDocs, t])
|
||||||
|
|
||||||
|
// Only fetch documents when the tab becomes visible for the first time
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDocuments()
|
if (isDocumentsTabVisible && !initialLoadRef.current) {
|
||||||
}, [fetchDocuments, t])
|
fetchDocuments()
|
||||||
|
initialLoadRef.current = true
|
||||||
|
}
|
||||||
|
}, [isDocumentsTabVisible, fetchDocuments])
|
||||||
|
|
||||||
const scanDocuments = useCallback(async () => {
|
const scanDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -63,19 +71,22 @@ export default function DocumentManager() {
|
|||||||
}
|
}
|
||||||
}, [t])
|
}, [t])
|
||||||
|
|
||||||
|
// Only set up polling when the tab is visible and health is good
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isDocumentsTabVisible || !health) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
if (!health) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await fetchDocuments()
|
await fetchDocuments()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) }))
|
||||||
}
|
}
|
||||||
}, 5000)
|
}, 5000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [health, fetchDocuments, t])
|
}, [health, fetchDocuments, t, isDocumentsTabVisible])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="!size-full !rounded-none !border-none">
|
<Card className="!size-full !rounded-none !border-none">
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
// import { MiniMap } from '@react-sigma/minimap'
|
// import { MiniMap } from '@react-sigma/minimap'
|
||||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||||
@@ -107,10 +108,17 @@ const GraphEvents = () => {
|
|||||||
const GraphViewer = () => {
|
const GraphViewer = () => {
|
||||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||||
const sigmaRef = useRef<any>(null)
|
const sigmaRef = useRef<any>(null)
|
||||||
|
const initAttemptedRef = useRef(false)
|
||||||
|
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||||
|
const isFetching = useGraphStore.use.isFetching()
|
||||||
|
const shouldRender = useGraphStore.use.shouldRender() // Rendering control state
|
||||||
|
|
||||||
|
// Get tab visibility
|
||||||
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
||||||
|
|
||||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
||||||
@@ -120,6 +128,15 @@ const GraphViewer = () => {
|
|||||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||||
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
||||||
|
|
||||||
|
// Ensure rendering is enabled when tab becomes visible
|
||||||
|
useEffect(() => {
|
||||||
|
if (isGraphTabVisible && !shouldRender && !isFetching && !initAttemptedRef.current) {
|
||||||
|
// If tab is visible but graph is not rendering, try to enable rendering
|
||||||
|
useGraphStore.getState().setShouldRender(true)
|
||||||
|
initAttemptedRef.current = true
|
||||||
|
}
|
||||||
|
}, [isGraphTabVisible, shouldRender, isFetching])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSigmaSettings({
|
setSigmaSettings({
|
||||||
...defaultSigmaSettings,
|
...defaultSigmaSettings,
|
||||||
@@ -148,6 +165,24 @@ const GraphViewer = () => {
|
|||||||
[selectedNode]
|
[selectedNode]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If we shouldn't render, show loading state or empty state
|
||||||
|
if (!shouldRender) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center bg-background">
|
||||||
|
{isFetching ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||||
|
<p>Reloading Graph Data...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
{/* Empty or hint message */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SigmaContainer
|
<SigmaContainer
|
||||||
settings={sigmaSettings}
|
settings={sigmaSettings}
|
||||||
|
@@ -6,6 +6,7 @@ import { useGraphStore, RawGraph } from '@/stores/graph'
|
|||||||
import { queryGraphs } from '@/api/lightrag'
|
import { queryGraphs } from '@/api/lightrag'
|
||||||
import { useBackendState } from '@/stores/state'
|
import { useBackendState } from '@/stores/state'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
||||||
|
|
||||||
import seedrandom from 'seedrandom'
|
import seedrandom from 'seedrandom'
|
||||||
|
|
||||||
@@ -169,25 +170,22 @@ const useLightrangeGraph = () => {
|
|||||||
const minDegree = useSettingsStore.use.graphMinDegree()
|
const minDegree = useSettingsStore.use.graphMinDegree()
|
||||||
const isFetching = useGraphStore.use.isFetching()
|
const isFetching = useGraphStore.use.isFetching()
|
||||||
|
|
||||||
// Use ref to track fetch status
|
// Get tab visibility
|
||||||
const fetchStatusRef = useRef<Record<string, boolean>>({});
|
const { isTabVisible } = useTabVisibility()
|
||||||
|
const isGraphTabVisible = isTabVisible('knowledge-graph')
|
||||||
|
|
||||||
// Track previous parameters to detect actual changes
|
// Track previous parameters to detect actual changes
|
||||||
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree });
|
const prevParamsRef = useRef({ queryLabel, maxQueryDepth, minDegree })
|
||||||
|
|
||||||
// Reset fetch status only when parameters actually change
|
// Use ref to track if data has been loaded and initial load
|
||||||
useEffect(() => {
|
const dataLoadedRef = useRef(false)
|
||||||
const prevParams = prevParamsRef.current;
|
const initialLoadRef = useRef(false)
|
||||||
if (prevParams.queryLabel !== queryLabel ||
|
|
||||||
prevParams.maxQueryDepth !== maxQueryDepth ||
|
// Check if parameters have changed
|
||||||
prevParams.minDegree !== minDegree) {
|
const paramsChanged =
|
||||||
useGraphStore.getState().setIsFetching(false);
|
prevParamsRef.current.queryLabel !== queryLabel ||
|
||||||
// Reset fetch status for new parameters
|
prevParamsRef.current.maxQueryDepth !== maxQueryDepth ||
|
||||||
fetchStatusRef.current = {};
|
prevParamsRef.current.minDegree !== minDegree
|
||||||
// Update previous parameters
|
|
||||||
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree };
|
|
||||||
}
|
|
||||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
|
||||||
|
|
||||||
const getNode = useCallback(
|
const getNode = useCallback(
|
||||||
(nodeId: string) => {
|
(nodeId: string) => {
|
||||||
@@ -203,77 +201,131 @@ const useLightrangeGraph = () => {
|
|||||||
[rawGraph]
|
[rawGraph]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Track if a fetch is in progress to prevent multiple simultaneous fetches
|
||||||
|
const fetchInProgressRef = useRef(false)
|
||||||
|
|
||||||
|
// Data fetching logic - use a separate effect with minimal dependencies to prevent multiple triggers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryLabel) {
|
// Skip if fetch is already in progress
|
||||||
const fetchKey = `${queryLabel}-${maxQueryDepth}-${minDegree}`;
|
if (fetchInProgressRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Only fetch if we haven't fetched this combination in the current component lifecycle
|
// If there's no query label, reset the graph only if it hasn't been reset already
|
||||||
if (!isFetching && !fetchStatusRef.current[fetchKey]) {
|
if (!queryLabel) {
|
||||||
const state = useGraphStore.getState();
|
if (rawGraph !== null || sigmaGraph !== null) {
|
||||||
// Clear selection and highlighted nodes before fetching new graph
|
const state = useGraphStore.getState()
|
||||||
state.clearSelection();
|
state.reset()
|
||||||
if (state.sigmaGraph) {
|
state.setSigmaGraph(new DirectedGraph())
|
||||||
state.sigmaGraph.forEachNode((node) => {
|
state.setGraphLabels(['*'])
|
||||||
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false);
|
// Reset fetch attempt flags when resetting graph
|
||||||
});
|
state.setGraphDataFetchAttempted(false)
|
||||||
}
|
state.setLabelsFetchAttempted(false)
|
||||||
|
}
|
||||||
|
dataLoadedRef.current = false
|
||||||
|
initialLoadRef.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state.setIsFetching(true);
|
// Check if we've already attempted to fetch this data in this session
|
||||||
fetchStatusRef.current[fetchKey] = true;
|
const graphDataFetchAttempted = useGraphStore.getState().graphDataFetchAttempted
|
||||||
fetchGraph(queryLabel, maxQueryDepth, minDegree).then((data) => {
|
|
||||||
const state = useGraphStore.getState()
|
|
||||||
const newSigmaGraph = createSigmaGraph(data)
|
|
||||||
data?.buildDynamicMap()
|
|
||||||
|
|
||||||
// Update all graph data at once to minimize UI flicker
|
// Fetch data if:
|
||||||
state.clearSelection()
|
// 1. We're not already fetching
|
||||||
state.setMoveToSelectedNode(false)
|
// 2. We haven't attempted to fetch in this session OR parameters have changed
|
||||||
state.setSigmaGraph(newSigmaGraph)
|
if (!isFetching && !fetchInProgressRef.current && (!graphDataFetchAttempted || paramsChanged)) {
|
||||||
state.setRawGraph(data)
|
// Set flag to prevent multiple fetches
|
||||||
|
fetchInProgressRef.current = true
|
||||||
|
// Set global flag to indicate we've attempted to fetch in this session
|
||||||
|
useGraphStore.getState().setGraphDataFetchAttempted(true)
|
||||||
|
|
||||||
// Extract labels from current graph data
|
const state = useGraphStore.getState()
|
||||||
if (data) {
|
|
||||||
const labelSet = new Set<string>();
|
// Set rendering control state
|
||||||
for (const node of data.nodes) {
|
state.setIsFetching(true)
|
||||||
if (node.labels && Array.isArray(node.labels)) {
|
state.setShouldRender(false) // Disable rendering during data loading
|
||||||
for (const label of node.labels) {
|
|
||||||
if (label !== '*') { // filter out label "*"
|
// Clear selection and highlighted nodes before fetching new graph
|
||||||
labelSet.add(label);
|
state.clearSelection()
|
||||||
}
|
if (state.sigmaGraph) {
|
||||||
|
state.sigmaGraph.forEachNode((node) => {
|
||||||
|
state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update parameter reference
|
||||||
|
prevParamsRef.current = { queryLabel, maxQueryDepth, minDegree }
|
||||||
|
|
||||||
|
console.log('Fetching graph data (once per session unless params change)...')
|
||||||
|
|
||||||
|
// Use a local copy of the parameters to avoid closure issues
|
||||||
|
const currentQueryLabel = queryLabel
|
||||||
|
const currentMaxQueryDepth = maxQueryDepth
|
||||||
|
const currentMinDegree = minDegree
|
||||||
|
|
||||||
|
fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMinDegree).then((data) => {
|
||||||
|
const state = useGraphStore.getState()
|
||||||
|
const newSigmaGraph = createSigmaGraph(data)
|
||||||
|
data?.buildDynamicMap()
|
||||||
|
|
||||||
|
// Update all graph data at once to minimize UI flicker
|
||||||
|
state.clearSelection()
|
||||||
|
state.setMoveToSelectedNode(false)
|
||||||
|
state.setSigmaGraph(newSigmaGraph)
|
||||||
|
state.setRawGraph(data)
|
||||||
|
|
||||||
|
// Extract labels from current graph data for local use
|
||||||
|
if (data) {
|
||||||
|
const labelSet = new Set<string>()
|
||||||
|
for (const node of data.nodes) {
|
||||||
|
if (node.labels && Array.isArray(node.labels)) {
|
||||||
|
for (const label of node.labels) {
|
||||||
|
if (label !== '*') { // filter out label "*"
|
||||||
|
labelSet.add(label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Put * on top of other labels
|
|
||||||
const sortedLabels = Array.from(labelSet).sort();
|
|
||||||
state.setGraphLabels(['*', ...sortedLabels]);
|
|
||||||
} else {
|
|
||||||
// Ensure * is there eventhough there is no graph data
|
|
||||||
state.setGraphLabels(['*']);
|
|
||||||
}
|
}
|
||||||
|
// Put * on top of other labels
|
||||||
|
const sortedLabels = Array.from(labelSet).sort()
|
||||||
|
state.setGraphLabels(['*', ...sortedLabels])
|
||||||
|
} else {
|
||||||
|
// Ensure * is there eventhough there is no graph data
|
||||||
|
state.setGraphLabels(['*'])
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all database labels after graph update
|
// Mark data as loaded and initial load completed
|
||||||
state.fetchAllDatabaseLabels();
|
dataLoadedRef.current = true
|
||||||
if (!data) {
|
initialLoadRef.current = true
|
||||||
// If data is invalid, remove the fetch flag to allow retry
|
fetchInProgressRef.current = false
|
||||||
delete fetchStatusRef.current[fetchKey];
|
|
||||||
}
|
// Reset camera view by triggering FocusOnNode component
|
||||||
// Reset fetching state after all updates are complete
|
state.setMoveToSelectedNode(true)
|
||||||
// Reset camera view by triggering FocusOnNode component
|
|
||||||
state.setMoveToSelectedNode(true);
|
// Enable rendering if the tab is visible
|
||||||
state.setIsFetching(false);
|
state.setShouldRender(isGraphTabVisible)
|
||||||
}).catch(() => {
|
state.setIsFetching(false)
|
||||||
// Reset fetching state and remove flag in case of error
|
}).catch((error) => {
|
||||||
useGraphStore.getState().setIsFetching(false);
|
console.error('Error fetching graph data:', error)
|
||||||
delete fetchStatusRef.current[fetchKey];
|
// Reset fetching state and remove flag in case of error
|
||||||
})
|
const state = useGraphStore.getState()
|
||||||
}
|
state.setIsFetching(false)
|
||||||
} else {
|
state.setShouldRender(isGraphTabVisible) // Restore rendering state
|
||||||
const state = useGraphStore.getState()
|
dataLoadedRef.current = false // Allow retry
|
||||||
state.reset()
|
fetchInProgressRef.current = false
|
||||||
state.setSigmaGraph(new DirectedGraph())
|
// Reset global flag to allow retry
|
||||||
state.setGraphLabels(['*'])
|
state.setGraphDataFetchAttempted(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [queryLabel, maxQueryDepth, minDegree, isFetching])
|
}, [queryLabel, maxQueryDepth, minDegree, isFetching, paramsChanged, isGraphTabVisible, rawGraph, sigmaGraph]) // Added missing dependencies
|
||||||
|
|
||||||
|
// Update rendering state when tab visibility changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Only update rendering state if data is loaded and not fetching
|
||||||
|
if (rawGraph) {
|
||||||
|
useGraphStore.getState().setShouldRender(isGraphTabVisible)
|
||||||
|
}
|
||||||
|
}, [isGraphTabVisible, rawGraph])
|
||||||
|
|
||||||
const lightrageGraph = useCallback(() => {
|
const lightrageGraph = useCallback(() => {
|
||||||
if (sigmaGraph) {
|
if (sigmaGraph) {
|
||||||
|
@@ -71,6 +71,11 @@ interface GraphState {
|
|||||||
|
|
||||||
moveToSelectedNode: boolean
|
moveToSelectedNode: boolean
|
||||||
isFetching: boolean
|
isFetching: boolean
|
||||||
|
shouldRender: boolean
|
||||||
|
|
||||||
|
// Global flags to track data fetching attempts
|
||||||
|
graphDataFetchAttempted: boolean
|
||||||
|
labelsFetchAttempted: boolean
|
||||||
|
|
||||||
refreshLayout: () => void
|
refreshLayout: () => void
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
||||||
@@ -88,6 +93,11 @@ interface GraphState {
|
|||||||
setAllDatabaseLabels: (labels: string[]) => void
|
setAllDatabaseLabels: (labels: string[]) => void
|
||||||
fetchAllDatabaseLabels: () => Promise<void>
|
fetchAllDatabaseLabels: () => Promise<void>
|
||||||
setIsFetching: (isFetching: boolean) => void
|
setIsFetching: (isFetching: boolean) => void
|
||||||
|
setShouldRender: (shouldRender: boolean) => void
|
||||||
|
|
||||||
|
// Methods to set global flags
|
||||||
|
setGraphDataFetchAttempted: (attempted: boolean) => void
|
||||||
|
setLabelsFetchAttempted: (attempted: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
||||||
@@ -98,6 +108,11 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
|
|
||||||
moveToSelectedNode: false,
|
moveToSelectedNode: false,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
|
shouldRender: false,
|
||||||
|
|
||||||
|
// Initialize global flags
|
||||||
|
graphDataFetchAttempted: false,
|
||||||
|
labelsFetchAttempted: false,
|
||||||
|
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
@@ -116,6 +131,7 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||||
|
setShouldRender: (shouldRender: boolean) => set({ shouldRender }),
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) =>
|
||||||
set({ selectedNode: nodeId, moveToSelectedNode }),
|
set({ selectedNode: nodeId, moveToSelectedNode }),
|
||||||
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
setFocusedNode: (nodeId: string | null) => set({ focusedNode: nodeId }),
|
||||||
@@ -137,7 +153,8 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
graphLabels: ['*'],
|
graphLabels: ['*'],
|
||||||
moveToSelectedNode: false
|
moveToSelectedNode: false,
|
||||||
|
shouldRender: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setRawGraph: (rawGraph: RawGraph | null) =>
|
setRawGraph: (rawGraph: RawGraph | null) =>
|
||||||
@@ -153,15 +170,22 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
|
|
||||||
fetchAllDatabaseLabels: async () => {
|
fetchAllDatabaseLabels: async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Fetching all database labels...');
|
||||||
const labels = await getGraphLabels();
|
const labels = await getGraphLabels();
|
||||||
set({ allDatabaseLabels: ['*', ...labels] });
|
set({ allDatabaseLabels: ['*', ...labels] });
|
||||||
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch all database labels:', error);
|
console.error('Failed to fetch all database labels:', error);
|
||||||
set({ allDatabaseLabels: ['*'] });
|
set({ allDatabaseLabels: ['*'] });
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode })
|
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
|
||||||
|
|
||||||
|
// Methods to set global flags
|
||||||
|
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
|
||||||
|
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const useGraphStore = createSelectors(useGraphStoreBase)
|
const useGraphStore = createSelectors(useGraphStoreBase)
|
||||||
|
Reference in New Issue
Block a user