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
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue