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:
yangdx 2025-09-22 03:03:53 +08:00
parent 0fcb5af333
commit 8af097a8e4
3 changed files with 101 additions and 23 deletions

View file

@ -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;

View file

@ -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 &&

View file

@ -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>
)} )}