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
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])
/**
@ -189,6 +198,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
}
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize])
/**
* When component mount or hovered node change
* => Setting the sigma reducers
@ -208,6 +218,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
// Node reducer for node appearance
nodeReducer: (node, data) => {
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 & {
labelColor?: string
borderColor?: string
@ -228,11 +245,17 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
}
} catch (error) {
console.error('Error in nodeReducer:', error);
return { ...data, highlighted: false, labelColor }
}
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
if (graph.extremities(_focusedEdge).includes(node)) {
newData.highlighted = true
newData.size = 3
try {
if (graph.extremities(_focusedEdge).includes(node)) {
newData.highlighted = true
newData.size = 3
}
} catch (error) {
console.error('Error accessing edge extremities in nodeReducer:', error);
return { ...data, highlighted: false, labelColor }
}
} else {
return newData
@ -252,6 +275,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
// Edge reducer for edge appearance
edgeReducer: (edge, data) => {
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 }
if (!disableHoverEffect) {
@ -270,6 +300,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
}
} catch (error) {
console.error('Error in edgeReducer:', error);
return { ...data, hidden: false, labelColor, color: edgeColor }
}
} else {
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null;

View file

@ -1,7 +1,6 @@
import { FC, useCallback, useEffect } from 'react'
import {
EdgeById,
NodeById,
GraphSearchInputProps,
GraphSearchContextProviderProps
} from '@react-sigma/graph-search'
@ -23,10 +22,31 @@ export interface OptionItem {
const NodeOption = ({ id }: { id: string }) => {
const graph = useGraphStore.use.sigmaGraph()
// Early return if no graph or node doesn't exist
if (!graph?.hasNode(id)) {
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) {
@ -83,12 +103,17 @@ export const GraphSearchInput = ({
}
})
// Add nodes to search engine
const documents = graph.nodes().map((id: string) => ({
id: id,
label: graph.getNodeAttribute(id, 'label')
}))
newSearchEngine.addAll(documents)
// Add nodes to search engine with safety checks
const documents = graph.nodes()
.filter(id => graph.hasNode(id)) // Ensure node exists before accessing attributes
.map((id: string) => ({
id: id,
label: graph.getNodeAttribute(id, 'label')
}))
if (documents.length > 0) {
newSearchEngine.addAll(documents)
}
// Update search engine in store
useGraphStore.getState().setSearchEngine(newSearchEngine)
@ -136,13 +161,16 @@ export const GraphSearchInput = ({
// Get already matched IDs to avoid duplicates
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()
.filter(id => {
// Skip already matched nodes
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')
// Match if label contains query string but doesn't start with it
return label &&

View file

@ -108,8 +108,9 @@ const GraphEvents = () => {
}
const GraphViewer = () => {
const [sigmaSettings, setSigmaSettings] = useState<Partial<SigmaSettings>>({})
const [isThemeSwitching, setIsThemeSwitching] = useState(false)
const sigmaRef = useRef<any>(null)
const prevTheme = useRef<string>('')
const selectedNode = useGraphStore.use.selectedNode()
const focusedNode = useGraphStore.use.focusedNode()
@ -122,11 +123,29 @@ const GraphViewer = () => {
const showLegend = useSettingsStore.use.showLegend()
const theme = useSettingsStore.use.theme()
// Initialize sigma settings based on theme
useEffect(() => {
// Memoize sigma settings to prevent unnecessary re-creation
const memoizedSigmaSettings = useMemo(() => {
const isDarkTheme = theme === 'dark'
const settings = createSigmaSettings(isDarkTheme)
setSigmaSettings(settings)
return createSigmaSettings(isDarkTheme)
}, [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)
}, [theme])
@ -176,7 +195,7 @@ const GraphViewer = () => {
return (
<div className="relative h-full w-full overflow-hidden">
<SigmaContainer
settings={sigmaSettings}
settings={memoizedSigmaSettings}
className="!bg-background !size-full overflow-hidden"
ref={sigmaRef}
>
@ -188,7 +207,7 @@ const GraphViewer = () => {
<div className="absolute top-2 left-2 flex items-start gap-2">
<GraphLabels />
{showNodeSearchBar && (
{showNodeSearchBar && !isThemeSwitching && (
<GraphSearch
value={searchInitSelectedNode}
onFocus={onSearchFocus}
@ -225,12 +244,12 @@ const GraphViewer = () => {
<SettingsDisplay />
</SigmaContainer>
{/* Loading overlay - shown when data is loading */}
{isFetching && (
{/* Loading overlay - shown when data is loading or theme is switching */}
{(isFetching || isThemeSwitching) && (
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
<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 Graph Data...</p>
<p>{isThemeSwitching ? 'Switching Theme...' : 'Loading Graph Data...'}</p>
</div>
</div>
)}