Avoid graphics flickering during node operations
This commit is contained in:
@@ -13,23 +13,37 @@ const FocusOnNode = ({ node, move }: { node: string | null; move?: boolean }) =>
|
|||||||
* When the selected item changes, highlighted the node and center the camera on it.
|
* When the selected item changes, highlighted the node and center the camera on it.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const graph = sigma.getGraph();
|
||||||
|
|
||||||
if (move) {
|
if (move) {
|
||||||
if (node) {
|
if (node && graph.hasNode(node)) {
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
try {
|
||||||
gotoNode(node)
|
graph.setNodeAttribute(node, 'highlighted', true);
|
||||||
|
gotoNode(node);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error focusing on node:', error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no node is selected but move is true, reset to default view
|
// If no node is selected but move is true, reset to default view
|
||||||
sigma.setCustomBBox(null)
|
sigma.setCustomBBox(null);
|
||||||
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 })
|
sigma.getCamera().animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 0 });
|
||||||
|
}
|
||||||
|
useGraphStore.getState().setMoveToSelectedNode(false);
|
||||||
|
} else if (node && graph.hasNode(node)) {
|
||||||
|
try {
|
||||||
|
graph.setNodeAttribute(node, 'highlighted', true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error highlighting node:', error);
|
||||||
}
|
}
|
||||||
useGraphStore.getState().setMoveToSelectedNode(false)
|
|
||||||
} else if (node) {
|
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (node) {
|
if (node && graph.hasNode(node)) {
|
||||||
sigma.getGraph().setNodeAttribute(node, 'highlighted', false)
|
try {
|
||||||
|
graph.setNodeAttribute(node, 'highlighted', false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up node highlight:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [node, move, sigma, gotoNode])
|
}, [node, move, sigma, gotoNode])
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { useLoadGraph, useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core'
|
||||||
import Graph from 'graphology'
|
import { AbstractGraph } from 'graphology-types'
|
||||||
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
// import { useLayoutCircular } from '@react-sigma/layout-circular'
|
||||||
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
@@ -25,7 +25,6 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
const sigma = useSigma<NodeType, EdgeType>()
|
const sigma = useSigma<NodeType, EdgeType>()
|
||||||
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
const registerEvents = useRegisterEvents<NodeType, EdgeType>()
|
||||||
const setSettings = useSetSettings<NodeType, EdgeType>()
|
const setSettings = useSetSettings<NodeType, EdgeType>()
|
||||||
const loadGraph = useLoadGraph<NodeType, EdgeType>()
|
|
||||||
|
|
||||||
const maxIterations = useSettingsStore.use.graphLayoutMaxIterations()
|
const maxIterations = useSettingsStore.use.graphLayoutMaxIterations()
|
||||||
const { assign: assignLayout } = useLayoutForceAtlas2({
|
const { assign: assignLayout } = useLayoutForceAtlas2({
|
||||||
@@ -45,14 +44,45 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount or maxIterations changes
|
* When component mount or maxIterations changes
|
||||||
* => load the graph and apply layout
|
* => ensure graph reference and apply layout
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sigmaGraph) {
|
if (sigmaGraph && sigma) {
|
||||||
loadGraph(sigmaGraph as unknown as Graph<NodeType, EdgeType>)
|
// 确保 sigma 实例内部的 graph 引用被更新
|
||||||
assignLayout()
|
try {
|
||||||
|
// 尝试直接设置 sigma 实例的 graph 引用
|
||||||
|
if (typeof sigma.setGraph === 'function') {
|
||||||
|
sigma.setGraph(sigmaGraph as unknown as AbstractGraph<NodeType, EdgeType>);
|
||||||
|
console.log('Directly set graph on sigma instance');
|
||||||
|
} else {
|
||||||
|
// 如果 setGraph 方法不存在,尝试直接设置 graph 属性
|
||||||
|
(sigma as any).graph = sigmaGraph;
|
||||||
|
console.log('Set graph property on sigma instance');
|
||||||
}
|
}
|
||||||
}, [assignLayout, loadGraph, sigmaGraph, maxIterations])
|
} catch (error) {
|
||||||
|
console.error('Error setting graph on sigma instance:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用布局
|
||||||
|
assignLayout();
|
||||||
|
console.log('Layout applied to graph');
|
||||||
|
}
|
||||||
|
}, [sigma, sigmaGraph, assignLayout, maxIterations])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the sigma instance is set in the store
|
||||||
|
* This provides a backup in case the instance wasn't set in GraphViewer
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (sigma) {
|
||||||
|
// Double-check that the store has the sigma instance
|
||||||
|
const currentInstance = useGraphStore.getState().sigmaInstance;
|
||||||
|
if (!currentInstance) {
|
||||||
|
console.log('Setting sigma instance from GraphControl (backup)');
|
||||||
|
useGraphStore.getState().setSigmaInstance(sigma);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [sigma]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When component mount
|
* When component mount
|
||||||
@@ -138,14 +168,18 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
const _focusedNode = focusedNode || selectedNode
|
const _focusedNode = focusedNode || selectedNode
|
||||||
const _focusedEdge = focusedEdge || selectedEdge
|
const _focusedEdge = focusedEdge || selectedEdge
|
||||||
|
|
||||||
if (_focusedNode) {
|
if (_focusedNode && graph.hasNode(_focusedNode)) {
|
||||||
|
try {
|
||||||
if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) {
|
if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) {
|
||||||
newData.highlighted = true
|
newData.highlighted = true
|
||||||
if (node === selectedNode) {
|
if (node === selectedNode) {
|
||||||
newData.borderColor = Constants.nodeBorderColorSelected
|
newData.borderColor = Constants.nodeBorderColorSelected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (_focusedEdge) {
|
} catch (error) {
|
||||||
|
console.error('Error in nodeReducer:', error);
|
||||||
|
}
|
||||||
|
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) {
|
||||||
if (graph.extremities(_focusedEdge).includes(node)) {
|
if (graph.extremities(_focusedEdge).includes(node)) {
|
||||||
newData.highlighted = true
|
newData.highlighted = true
|
||||||
newData.size = 3
|
newData.size = 3
|
||||||
@@ -173,7 +207,8 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
if (!disableHoverEffect) {
|
if (!disableHoverEffect) {
|
||||||
const _focusedNode = focusedNode || selectedNode
|
const _focusedNode = focusedNode || selectedNode
|
||||||
|
|
||||||
if (_focusedNode) {
|
if (_focusedNode && graph.hasNode(_focusedNode)) {
|
||||||
|
try {
|
||||||
if (hideUnselectedEdges) {
|
if (hideUnselectedEdges) {
|
||||||
if (!graph.extremities(edge).includes(_focusedNode)) {
|
if (!graph.extremities(edge).includes(_focusedNode)) {
|
||||||
newData.hidden = true
|
newData.hidden = true
|
||||||
@@ -183,11 +218,17 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
|
|||||||
newData.color = Constants.edgeColorHighlighted
|
newData.color = Constants.edgeColorHighlighted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in edgeReducer:', error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (focusedEdge || selectedEdge) {
|
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null;
|
||||||
if (edge === selectedEdge) {
|
const _focusedEdge = focusedEdge && graph.hasEdge(focusedEdge) ? focusedEdge : null;
|
||||||
|
|
||||||
|
if (_selectedEdge || _focusedEdge) {
|
||||||
|
if (edge === _selectedEdge) {
|
||||||
newData.color = Constants.edgeColorSelected
|
newData.color = Constants.edgeColorSelected
|
||||||
} else if (edge === focusedEdge) {
|
} else if (edge === _focusedEdge) {
|
||||||
newData.color = Constants.edgeColorHighlighted
|
newData.color = Constants.edgeColorHighlighted
|
||||||
} else if (hideUnselectedEdges) {
|
} else if (hideUnselectedEdges) {
|
||||||
newData.hidden = true
|
newData.hidden = true
|
||||||
|
@@ -90,11 +90,31 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
|
|||||||
const relationships = []
|
const relationships = []
|
||||||
|
|
||||||
if (state.sigmaGraph && state.rawGraph) {
|
if (state.sigmaGraph && state.rawGraph) {
|
||||||
for (const edgeId of state.sigmaGraph.edges(node.id)) {
|
try {
|
||||||
|
// 检查节点是否还存在
|
||||||
|
if (!state.sigmaGraph.hasNode(node.id)) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
relationships: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有边
|
||||||
|
const edges = state.sigmaGraph.edges(node.id)
|
||||||
|
|
||||||
|
// 处理每条边
|
||||||
|
for (const edgeId of edges) {
|
||||||
|
// 检查边是否还存在
|
||||||
|
if (!state.sigmaGraph.hasEdge(edgeId)) continue;
|
||||||
|
|
||||||
const edge = state.rawGraph.getEdge(edgeId, true)
|
const edge = state.rawGraph.getEdge(edgeId, true)
|
||||||
if (edge) {
|
if (edge) {
|
||||||
const isTarget = node.id === edge.source
|
const isTarget = node.id === edge.source
|
||||||
const neighbourId = isTarget ? edge.target : edge.source
|
const neighbourId = isTarget ? edge.target : edge.source
|
||||||
|
|
||||||
|
// 检查邻居节点是否存在
|
||||||
|
if (!state.sigmaGraph.hasNode(neighbourId)) continue;
|
||||||
|
|
||||||
const neighbour = state.rawGraph.getNode(neighbourId)
|
const neighbour = state.rawGraph.getNode(neighbourId)
|
||||||
if (neighbour) {
|
if (neighbour) {
|
||||||
relationships.push({
|
relationships.push({
|
||||||
@@ -105,7 +125,11 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refining node properties:', error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
relationships
|
relationships
|
||||||
@@ -114,8 +138,34 @@ const refineNodeProperties = (node: RawNodeType): NodeType => {
|
|||||||
|
|
||||||
const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
|
const refineEdgeProperties = (edge: RawEdgeType): EdgeType => {
|
||||||
const state = useGraphStore.getState()
|
const state = useGraphStore.getState()
|
||||||
const sourceNode = state.rawGraph?.getNode(edge.source)
|
let sourceNode: RawNodeType | undefined = undefined
|
||||||
const targetNode = state.rawGraph?.getNode(edge.target)
|
let targetNode: RawNodeType | undefined = undefined
|
||||||
|
|
||||||
|
if (state.sigmaGraph && state.rawGraph) {
|
||||||
|
try {
|
||||||
|
// 检查边是否还存在
|
||||||
|
if (!state.sigmaGraph.hasEdge(edge.id)) {
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
sourceNode: undefined,
|
||||||
|
targetNode: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查源节点是否存在
|
||||||
|
if (state.sigmaGraph.hasNode(edge.source)) {
|
||||||
|
sourceNode = state.rawGraph.getNode(edge.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查目标节点是否存在
|
||||||
|
if (state.sigmaGraph.hasNode(edge.target)) {
|
||||||
|
targetNode = state.rawGraph.getNode(edge.target)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refining edge properties:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...edge,
|
...edge,
|
||||||
sourceNode,
|
sourceNode,
|
||||||
|
@@ -15,16 +15,22 @@ export const TabVisibilityProvider: React.FC<TabVisibilityProviderProps> = ({ ch
|
|||||||
// Get current tab from settings store
|
// Get current tab from settings store
|
||||||
const currentTab = useSettingsStore.use.currentTab();
|
const currentTab = useSettingsStore.use.currentTab();
|
||||||
|
|
||||||
// Initialize visibility state with current tab as visible
|
// Initialize visibility state with all tabs visible
|
||||||
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
|
const [visibleTabs, setVisibleTabs] = useState<Record<string, boolean>>(() => ({
|
||||||
[currentTab]: true
|
'documents': true,
|
||||||
|
'knowledge-graph': true,
|
||||||
|
'retrieval': true,
|
||||||
|
'api': true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update visibility when current tab changes
|
// Keep all tabs visible when current tab changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisibleTabs((prev) => ({
|
setVisibleTabs((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[currentTab]: true
|
'documents': true,
|
||||||
|
'knowledge-graph': true,
|
||||||
|
'retrieval': true,
|
||||||
|
'api': true
|
||||||
}));
|
}));
|
||||||
}, [currentTab]);
|
}, [currentTab]);
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
import { useEffect, useLayoutEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||||
import { useTabVisibility } from '@/contexts/useTabVisibility'
|
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'
|
||||||
@@ -148,6 +148,49 @@ const GraphViewer = () => {
|
|||||||
setSigmaSettings(defaultSigmaSettings)
|
setSigmaSettings(defaultSigmaSettings)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Clean up sigma instance when component unmounts
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Clear the sigma instance when component unmounts
|
||||||
|
useGraphStore.getState().setSigmaInstance(null);
|
||||||
|
console.log('Cleared sigma instance on unmount');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get the sigmaGraph from the store
|
||||||
|
const sigmaGraph = useGraphStore.use.sigmaGraph();
|
||||||
|
|
||||||
|
// Set the sigma instance in the graph store when it's available
|
||||||
|
// Using useLayoutEffect to ensure this runs before child components need the instance
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (sigmaRef.current?.sigma) {
|
||||||
|
const instance = sigmaRef.current.sigma;
|
||||||
|
|
||||||
|
// Get the sigma instance from the ref and store it
|
||||||
|
console.log('Setting sigma instance in graph store (layout effect)');
|
||||||
|
useGraphStore.getState().setSigmaInstance(instance);
|
||||||
|
|
||||||
|
// If we also have a graph, bind it to the sigma instance
|
||||||
|
if (sigmaGraph) {
|
||||||
|
try {
|
||||||
|
// Try to set the graph on the sigma instance
|
||||||
|
if (typeof instance.setGraph === 'function') {
|
||||||
|
instance.setGraph(sigmaGraph);
|
||||||
|
console.log('Directly set graph on sigma instance in GraphViewer');
|
||||||
|
} else {
|
||||||
|
// If setGraph method doesn't exist, try to set the graph property directly
|
||||||
|
(instance as any).graph = sigmaGraph;
|
||||||
|
console.log('Set graph property on sigma instance in GraphViewer');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting graph on sigma instance in GraphViewer:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We want this to run when either the ref or the graph changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [sigmaRef.current, sigmaGraph]);
|
||||||
|
|
||||||
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
||||||
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
||||||
else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id)
|
else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id)
|
||||||
@@ -167,12 +210,9 @@ const GraphViewer = () => {
|
|||||||
[selectedNode]
|
[selectedNode]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Since TabsContent now forces mounting of all tabs, we need to conditionally render
|
// Always render SigmaContainer but control its visibility with CSS
|
||||||
// the SigmaContainer based on visibility to avoid unnecessary rendering
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
{/* Only render the SigmaContainer when the tab is visible */}
|
|
||||||
{isGraphTabVisible ? (
|
|
||||||
<SigmaContainer
|
<SigmaContainer
|
||||||
settings={sigmaSettings}
|
settings={sigmaSettings}
|
||||||
className="!bg-background !size-full overflow-hidden"
|
className="!bg-background !size-full overflow-hidden"
|
||||||
@@ -215,14 +255,6 @@ const GraphViewer = () => {
|
|||||||
|
|
||||||
<SettingsDisplay />
|
<SettingsDisplay />
|
||||||
</SigmaContainer>
|
</SigmaContainer>
|
||||||
) : (
|
|
||||||
// Placeholder when tab is not visible
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="text-center text-muted-foreground">
|
|
||||||
{/* Placeholder content */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Loading overlay - shown when data is loading */}
|
{/* Loading overlay - shown when data is loading */}
|
||||||
{isFetching && (
|
{isFetching && (
|
||||||
|
@@ -589,7 +589,8 @@ const useLightrangeGraph = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the layout
|
// We need to keep the refreshLayout call because Sigma doesn't automatically detect
|
||||||
|
// changes to the DirectedGraph object. This is necessary to trigger a re-render.
|
||||||
useGraphStore.getState().refreshLayout();
|
useGraphStore.getState().refreshLayout();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -637,28 +638,27 @@ const useLightrangeGraph = () => {
|
|||||||
if (!nodeId || !sigmaGraph || !rawGraph) return;
|
if (!nodeId || !sigmaGraph || !rawGraph) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the node exists
|
const state = useGraphStore.getState();
|
||||||
|
|
||||||
|
// 1. 检查节点是否存在
|
||||||
if (!sigmaGraph.hasNode(nodeId)) {
|
if (!sigmaGraph.hasNode(nodeId)) {
|
||||||
console.error('Node not found:', nodeId);
|
console.error('Node not found:', nodeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all nodes that will be deleted (including isolated nodes)
|
// 2. 获取要删除的节点
|
||||||
const nodesToDelete = getNodesThatWillBeDeleted(nodeId, sigmaGraph);
|
const nodesToDelete = getNodesThatWillBeDeleted(nodeId, sigmaGraph);
|
||||||
|
|
||||||
// Check if we would delete all nodes in the graph
|
// 3. 检查是否会删除所有节点
|
||||||
if (nodesToDelete.size === sigmaGraph.nodes().length) {
|
if (nodesToDelete.size === sigmaGraph.nodes().length) {
|
||||||
toast.error(t('graphPanel.propertiesView.node.deleteAllNodesError'));
|
toast.error(t('graphPanel.propertiesView.node.deleteAllNodesError'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the node is selected or focused, clear selection
|
// 4. 清除选中状态 - 这会导致PropertiesView立即关闭
|
||||||
const state = useGraphStore.getState();
|
|
||||||
if (state.selectedNode === nodeId || state.focusedNode === nodeId) {
|
|
||||||
state.clearSelection();
|
state.clearSelection();
|
||||||
}
|
|
||||||
|
|
||||||
// Process all nodes that need to be deleted
|
// 5. 删除节点和相关边
|
||||||
for (const nodeToDelete of nodesToDelete) {
|
for (const nodeToDelete of nodesToDelete) {
|
||||||
// Remove the node from the sigma graph (this will also remove connected edges)
|
// Remove the node from the sigma graph (this will also remove connected edges)
|
||||||
sigmaGraph.dropNode(nodeToDelete);
|
sigmaGraph.dropNode(nodeToDelete);
|
||||||
@@ -713,7 +713,8 @@ const useLightrangeGraph = () => {
|
|||||||
toast.info(t('graphPanel.propertiesView.node.nodesRemoved', { count: nodesToDelete.size }));
|
toast.info(t('graphPanel.propertiesView.node.nodesRemoved', { count: nodesToDelete.size }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force a refresh of the graph layout
|
// We need to keep the refreshLayout call because Sigma doesn't automatically detect
|
||||||
|
// changes to the DirectedGraph object. This is necessary to trigger a re-render.
|
||||||
useGraphStore.getState().refreshLayout();
|
useGraphStore.getState().refreshLayout();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@@ -66,6 +66,7 @@ interface GraphState {
|
|||||||
|
|
||||||
rawGraph: RawGraph | null
|
rawGraph: RawGraph | null
|
||||||
sigmaGraph: DirectedGraph | null
|
sigmaGraph: DirectedGraph | null
|
||||||
|
sigmaInstance: any | null
|
||||||
allDatabaseLabels: string[]
|
allDatabaseLabels: string[]
|
||||||
|
|
||||||
moveToSelectedNode: boolean
|
moveToSelectedNode: boolean
|
||||||
@@ -77,6 +78,7 @@ interface GraphState {
|
|||||||
labelsFetchAttempted: boolean
|
labelsFetchAttempted: boolean
|
||||||
|
|
||||||
refreshLayout: () => void
|
refreshLayout: () => void
|
||||||
|
setSigmaInstance: (instance: any) => void
|
||||||
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
setSelectedNode: (nodeId: string | null, moveToSelectedNode?: boolean) => void
|
||||||
setFocusedNode: (nodeId: string | null) => void
|
setFocusedNode: (nodeId: string | null) => void
|
||||||
setSelectedEdge: (edgeId: string | null) => void
|
setSelectedEdge: (edgeId: string | null) => void
|
||||||
@@ -122,17 +124,45 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
|
|
||||||
rawGraph: null,
|
rawGraph: null,
|
||||||
sigmaGraph: null,
|
sigmaGraph: null,
|
||||||
|
sigmaInstance: null,
|
||||||
allDatabaseLabels: ['*'],
|
allDatabaseLabels: ['*'],
|
||||||
|
|
||||||
refreshLayout: () => {
|
refreshLayout: () => {
|
||||||
const currentGraph = get().sigmaGraph;
|
const { sigmaInstance, sigmaGraph } = get();
|
||||||
if (currentGraph) {
|
|
||||||
get().clearSelection();
|
// Debug information to help diagnose issues
|
||||||
get().setSigmaGraph(null);
|
console.log('refreshLayout called with:', {
|
||||||
setTimeout(() => {
|
hasSigmaInstance: !!sigmaInstance,
|
||||||
get().setSigmaGraph(currentGraph);
|
hasSigmaGraph: !!sigmaGraph,
|
||||||
}, 10);
|
sigmaInstanceType: sigmaInstance ? typeof sigmaInstance : 'null',
|
||||||
|
sigmaGraphNodeCount: sigmaGraph ? sigmaGraph.order : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sigmaInstance && sigmaGraph) {
|
||||||
|
try {
|
||||||
|
// 先尝试直接刷新
|
||||||
|
if (typeof sigmaInstance.refresh === 'function') {
|
||||||
|
sigmaInstance.refresh();
|
||||||
|
console.log('Graph refreshed using sigma.refresh()');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有refresh方法,尝试重新绑定graph
|
||||||
|
if (typeof sigmaInstance.setGraph === 'function') {
|
||||||
|
sigmaInstance.setGraph(sigmaGraph);
|
||||||
|
console.log('Rebound graph to sigma instance');
|
||||||
|
} else {
|
||||||
|
// 如果setGraph方法不存在,尝试直接设置graph属性
|
||||||
|
(sigmaInstance as any).graph = sigmaGraph;
|
||||||
|
console.log('Set graph property directly on sigma instance');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during refresh:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知UI需要重新渲染
|
||||||
|
set(state => ({ ...state }));
|
||||||
},
|
},
|
||||||
|
|
||||||
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
setIsFetching: (isFetching: boolean) => set({ isFetching }),
|
||||||
@@ -198,6 +228,8 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
|
|
||||||
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
|
setMoveToSelectedNode: (moveToSelectedNode?: boolean) => set({ moveToSelectedNode }),
|
||||||
|
|
||||||
|
setSigmaInstance: (instance: any) => set({ sigmaInstance: instance }),
|
||||||
|
|
||||||
// Methods to set global flags
|
// Methods to set global flags
|
||||||
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
|
setGraphDataFetchAttempted: (attempted: boolean) => set({ graphDataFetchAttempted: attempted }),
|
||||||
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted }),
|
setLabelsFetchAttempted: (attempted: boolean) => set({ labelsFetchAttempted: attempted }),
|
||||||
@@ -457,6 +489,9 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||||||
state.rawGraph.buildDynamicMap();
|
state.rawGraph.buildDynamicMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh the layout to update the visualization
|
||||||
|
state.refreshLayout();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error pruning node:', error);
|
console.error('Error pruning node:', error);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user