fix(webui): resolve theme switching crashes and optimize graph rendering
- Implement theme switching isolation to prevent component access during transitions by hidding GraphSearch - Fix NotFoundGraphError during theme switching by adding defensive programming - Replace problematic NodeById component with custom safe implementation - Add comprehensive error handling and node/edge existence checks - Optimize sigma settings with memoization to prevent unnecessary re-renders - Remove redundant theme update logic and simplify event listener management
This commit is contained in:
parent
0fcb5af333
commit
8af097a8e4
3 changed files with 101 additions and 23 deletions
|
|
@ -142,6 +142,15 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
|
|
||||||
// Register the events
|
// Register the events
|
||||||
registerEvents(events)
|
registerEvents(events)
|
||||||
|
|
||||||
|
// Cleanup function - basic cleanup without relying on specific APIs
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
console.log('Cleaning up graph event listeners')
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error cleaning up graph event listeners:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [registerEvents, enableEdgeEvents, sigma])
|
}, [registerEvents, enableEdgeEvents, sigma])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -189,6 +198,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
|
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or hovered node change
|
* When component mount or hovered node change
|
||||||
* => Setting the sigma reducers
|
* => Setting the sigma reducers
|
||||||
|
|
@ -208,6 +218,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
// Node reducer for node appearance
|
// Node reducer for node appearance
|
||||||
nodeReducer: (node, data) => {
|
nodeReducer: (node, data) => {
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
|
|
||||||
|
// Add defensive check for node existence during theme switching
|
||||||
|
if (!graph.hasNode(node)) {
|
||||||
|
console.warn(`Node ${node} not found in graph during theme switch, returning default data`)
|
||||||
|
return { ...data, highlighted: false, labelColor }
|
||||||
|
}
|
||||||
|
|
||||||
const newData: NodeType & {
|
const newData: NodeType & {
|
||||||
labelColor?: string
|
labelColor?: string
|
||||||
borderColor?: string
|
borderColor?: string
|
||||||
|
|
@ -228,11 +245,17 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in nodeReducer:', error);
|
console.error('Error in nodeReducer:', error);
|
||||||
|
return { ...data, highlighted: false, labelColor }
|
||||||
}
|
}
|
||||||
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
|
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
|
||||||
if (graph.extremities(_focusedEdge).includes(node)) {
|
try {
|
||||||
newData.highlighted = true
|
if (graph.extremities(_focusedEdge).includes(node)) {
|
||||||
newData.size = 3
|
newData.highlighted = true
|
||||||
|
newData.size = 3
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error accessing edge extremities in nodeReducer:', error);
|
||||||
|
return { ...data, highlighted: false, labelColor }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return newData
|
return newData
|
||||||
|
|
@ -252,6 +275,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
// Edge reducer for edge appearance
|
// Edge reducer for edge appearance
|
||||||
edgeReducer: (edge, data) => {
|
edgeReducer: (edge, data) => {
|
||||||
const graph = sigma.getGraph()
|
const graph = sigma.getGraph()
|
||||||
|
|
||||||
|
// Add defensive check for edge existence during theme switching
|
||||||
|
if (!graph.hasEdge(edge)) {
|
||||||
|
console.warn(`Edge ${edge} not found in graph during theme switch, returning default data`)
|
||||||
|
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||||
|
}
|
||||||
|
|
||||||
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
const newData = { ...data, hidden: false, labelColor, color: edgeColor }
|
||||||
|
|
||||||
if (!disableHoverEffect) {
|
if (!disableHoverEffect) {
|
||||||
|
|
@ -270,6 +300,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in edgeReducer:', error);
|
console.error('Error in edgeReducer:', error);
|
||||||
|
return { ...data, hidden: false, labelColor, color: edgeColor }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null;
|
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { FC, useCallback, useEffect } from 'react'
|
import { FC, useCallback, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
EdgeById,
|
EdgeById,
|
||||||
NodeById,
|
|
||||||
GraphSearchInputProps,
|
GraphSearchInputProps,
|
||||||
GraphSearchContextProviderProps
|
GraphSearchContextProviderProps
|
||||||
} from '@react-sigma/graph-search'
|
} from '@react-sigma/graph-search'
|
||||||
|
|
@ -23,10 +22,31 @@ export interface OptionItem {
|
||||||
|
|
||||||
const NodeOption = ({ id }: { id: string }) => {
|
const NodeOption = ({ id }: { id: string }) => {
|
||||||
const graph = useGraphStore.use.sigmaGraph()
|
const graph = useGraphStore.use.sigmaGraph()
|
||||||
|
|
||||||
|
// Early return if no graph or node doesn't exist
|
||||||
if (!graph?.hasNode(id)) {
|
if (!graph?.hasNode(id)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return <NodeById id={id} />
|
|
||||||
|
// Safely get node attributes with fallbacks
|
||||||
|
const label = graph.getNodeAttribute(id, 'label') || id
|
||||||
|
const color = graph.getNodeAttribute(id, 'color') || '#666'
|
||||||
|
const size = graph.getNodeAttribute(id, 'size') || 4
|
||||||
|
|
||||||
|
// Custom node display component that doesn't rely on @react-sigma/graph-search
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 p-2 text-sm">
|
||||||
|
<div
|
||||||
|
className="rounded-full flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
width: Math.max(8, Math.min(size * 2, 16)),
|
||||||
|
height: Math.max(8, Math.min(size * 2, 16)),
|
||||||
|
backgroundColor: color
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="truncate">{label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function OptionComponent(item: OptionItem) {
|
function OptionComponent(item: OptionItem) {
|
||||||
|
|
@ -83,12 +103,17 @@ export const GraphSearchInput = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add nodes to search engine
|
// Add nodes to search engine with safety checks
|
||||||
const documents = graph.nodes().map((id: string) => ({
|
const documents = graph.nodes()
|
||||||
id: id,
|
.filter(id => graph.hasNode(id)) // Ensure node exists before accessing attributes
|
||||||
label: graph.getNodeAttribute(id, 'label')
|
.map((id: string) => ({
|
||||||
}))
|
id: id,
|
||||||
newSearchEngine.addAll(documents)
|
label: graph.getNodeAttribute(id, 'label')
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (documents.length > 0) {
|
||||||
|
newSearchEngine.addAll(documents)
|
||||||
|
}
|
||||||
|
|
||||||
// Update search engine in store
|
// Update search engine in store
|
||||||
useGraphStore.getState().setSearchEngine(newSearchEngine)
|
useGraphStore.getState().setSearchEngine(newSearchEngine)
|
||||||
|
|
@ -136,13 +161,16 @@ export const GraphSearchInput = ({
|
||||||
// Get already matched IDs to avoid duplicates
|
// Get already matched IDs to avoid duplicates
|
||||||
const matchedIds = new Set(result.map(item => item.id))
|
const matchedIds = new Set(result.map(item => item.id))
|
||||||
|
|
||||||
// Perform middle-content matching on all nodes
|
// Perform middle-content matching on all nodes with safety checks
|
||||||
const middleMatchResults = graph.nodes()
|
const middleMatchResults = graph.nodes()
|
||||||
.filter(id => {
|
.filter(id => {
|
||||||
// Skip already matched nodes
|
// Skip already matched nodes
|
||||||
if (matchedIds.has(id)) return false
|
if (matchedIds.has(id)) return false
|
||||||
|
|
||||||
// Get node label
|
// Ensure node exists before accessing attributes
|
||||||
|
if (!graph.hasNode(id)) return false
|
||||||
|
|
||||||
|
// Get node label safely
|
||||||
const label = graph.getNodeAttribute(id, 'label')
|
const label = graph.getNodeAttribute(id, 'label')
|
||||||
// Match if label contains query string but doesn't start with it
|
// Match if label contains query string but doesn't start with it
|
||||||
return label &&
|
return label &&
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,9 @@ const GraphEvents = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphViewer = () => {
|
const GraphViewer = () => {
|
||||||
const [sigmaSettings, setSigmaSettings] = useState<Partial<SigmaSettings>>({})
|
const [isThemeSwitching, setIsThemeSwitching] = useState(false)
|
||||||
const sigmaRef = useRef<any>(null)
|
const sigmaRef = useRef<any>(null)
|
||||||
|
const prevTheme = useRef<string>('')
|
||||||
|
|
||||||
const selectedNode = useGraphStore.use.selectedNode()
|
const selectedNode = useGraphStore.use.selectedNode()
|
||||||
const focusedNode = useGraphStore.use.focusedNode()
|
const focusedNode = useGraphStore.use.focusedNode()
|
||||||
|
|
@ -122,11 +123,29 @@ const GraphViewer = () => {
|
||||||
const showLegend = useSettingsStore.use.showLegend()
|
const showLegend = useSettingsStore.use.showLegend()
|
||||||
const theme = useSettingsStore.use.theme()
|
const theme = useSettingsStore.use.theme()
|
||||||
|
|
||||||
// Initialize sigma settings based on theme
|
// Memoize sigma settings to prevent unnecessary re-creation
|
||||||
useEffect(() => {
|
const memoizedSigmaSettings = useMemo(() => {
|
||||||
const isDarkTheme = theme === 'dark'
|
const isDarkTheme = theme === 'dark'
|
||||||
const settings = createSigmaSettings(isDarkTheme)
|
return createSigmaSettings(isDarkTheme)
|
||||||
setSigmaSettings(settings)
|
}, [theme])
|
||||||
|
|
||||||
|
// Initialize sigma settings based on theme with theme switching protection
|
||||||
|
useEffect(() => {
|
||||||
|
// Detect theme change
|
||||||
|
const isThemeChange = prevTheme.current && prevTheme.current !== theme
|
||||||
|
if (isThemeChange) {
|
||||||
|
setIsThemeSwitching(true)
|
||||||
|
console.log('Theme switching detected:', prevTheme.current, '->', theme)
|
||||||
|
|
||||||
|
// Reset theme switching state after a short delay
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsThemeSwitching(false)
|
||||||
|
console.log('Theme switching completed')
|
||||||
|
}, 150)
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
prevTheme.current = theme
|
||||||
console.log('Initialized sigma settings for theme:', theme)
|
console.log('Initialized sigma settings for theme:', theme)
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
|
|
@ -176,7 +195,7 @@ const GraphViewer = () => {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
<SigmaContainer
|
<SigmaContainer
|
||||||
settings={sigmaSettings}
|
settings={memoizedSigmaSettings}
|
||||||
className="!bg-background !size-full overflow-hidden"
|
className="!bg-background !size-full overflow-hidden"
|
||||||
ref={sigmaRef}
|
ref={sigmaRef}
|
||||||
>
|
>
|
||||||
|
|
@ -188,7 +207,7 @@ const GraphViewer = () => {
|
||||||
|
|
||||||
<div className="absolute top-2 left-2 flex items-start gap-2">
|
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||||
<GraphLabels />
|
<GraphLabels />
|
||||||
{showNodeSearchBar && (
|
{showNodeSearchBar && !isThemeSwitching && (
|
||||||
<GraphSearch
|
<GraphSearch
|
||||||
value={searchInitSelectedNode}
|
value={searchInitSelectedNode}
|
||||||
onFocus={onSearchFocus}
|
onFocus={onSearchFocus}
|
||||||
|
|
@ -225,12 +244,12 @@ const GraphViewer = () => {
|
||||||
<SettingsDisplay />
|
<SettingsDisplay />
|
||||||
</SigmaContainer>
|
</SigmaContainer>
|
||||||
|
|
||||||
{/* Loading overlay - shown when data is loading */}
|
{/* Loading overlay - shown when data is loading or theme is switching */}
|
||||||
{isFetching && (
|
{(isFetching || isThemeSwitching) && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
<div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
||||||
<p>Loading Graph Data...</p>
|
<p>{isThemeSwitching ? 'Switching Theme...' : 'Loading Graph Data...'}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue