fix graph trueRoot
This commit is contained in:
parent
30ee21f087
commit
c347581a6c
@ -1002,16 +1002,159 @@ class GraphManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @param {Set} excludedNodeIds - Node IDs to exclude from analysis (for simulation)
|
||||||
|
* @param {Set} excludedEdgeTypes - Edge types to exclude from traversal
|
||||||
|
* @param {Set} excludedNodeTypes - Node types to exclude from traversal
|
||||||
|
* @returns {Object} Analysis results with reachable/unreachable nodes
|
||||||
|
*/
|
||||||
|
analyzeGraphReachability(excludedNodeIds = new Set(), excludedEdgeTypes = new Set(), excludedNodeTypes = new Set()) {
|
||||||
|
console.log("Performing comprehensive reachability analysis...");
|
||||||
|
|
||||||
|
const analysis = {
|
||||||
|
reachableNodes: new Set(),
|
||||||
|
unreachableNodes: new Set(),
|
||||||
|
isolatedClusters: [],
|
||||||
|
affectedNodes: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.nodes.length === 0) return analysis;
|
||||||
|
|
||||||
|
// Build adjacency list excluding specified elements
|
||||||
|
const adjacencyList = {};
|
||||||
|
this.nodes.getIds().forEach(id => {
|
||||||
|
if (!excludedNodeIds.has(id)) {
|
||||||
|
adjacencyList[id] = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.edges.forEach(edge => {
|
||||||
|
const edgeType = edge.metadata?.relationship_type || '';
|
||||||
|
if (!excludedEdgeTypes.has(edgeType) &&
|
||||||
|
!excludedNodeIds.has(edge.from) &&
|
||||||
|
!excludedNodeIds.has(edge.to)) {
|
||||||
|
|
||||||
|
if (adjacencyList[edge.from]) {
|
||||||
|
adjacencyList[edge.from].push(edge.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// BFS traversal from true roots
|
||||||
|
const traversalQueue = [];
|
||||||
|
|
||||||
|
// Start from true roots that aren't excluded
|
||||||
|
this.trueRootIds.forEach(rootId => {
|
||||||
|
if (!excludedNodeIds.has(rootId)) {
|
||||||
|
const node = this.nodes.get(rootId);
|
||||||
|
if (node && !excludedNodeTypes.has(node.type)) {
|
||||||
|
if (!analysis.reachableNodes.has(rootId)) {
|
||||||
|
traversalQueue.push(rootId);
|
||||||
|
analysis.reachableNodes.add(rootId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// BFS to find all reachable nodes
|
||||||
|
let queueIndex = 0;
|
||||||
|
while (queueIndex < traversalQueue.length) {
|
||||||
|
const currentNode = traversalQueue[queueIndex++];
|
||||||
|
|
||||||
|
for (const neighbor of (adjacencyList[currentNode] || [])) {
|
||||||
|
if (!analysis.reachableNodes.has(neighbor)) {
|
||||||
|
const node = this.nodes.get(neighbor);
|
||||||
|
if (node && !excludedNodeTypes.has(node.type)) {
|
||||||
|
analysis.reachableNodes.add(neighbor);
|
||||||
|
traversalQueue.push(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify unreachable nodes (maintaining forensic integrity)
|
||||||
|
Object.keys(adjacencyList).forEach(nodeId => {
|
||||||
|
if (!analysis.reachableNodes.has(nodeId)) {
|
||||||
|
analysis.unreachableNodes.add(nodeId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find isolated clusters among unreachable nodes
|
||||||
|
analysis.isolatedClusters = this.findIsolatedClusters(
|
||||||
|
Array.from(analysis.unreachableNodes),
|
||||||
|
adjacencyList
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Reachability analysis complete:`, {
|
||||||
|
reachable: analysis.reachableNodes.size,
|
||||||
|
unreachable: analysis.unreachableNodes.size,
|
||||||
|
clusters: analysis.isolatedClusters.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get network statistics
|
* Find isolated clusters within a set of nodes
|
||||||
* @returns {Object} Statistics object
|
* Used for forensic analysis to identify disconnected subgraphs
|
||||||
|
*/
|
||||||
|
findIsolatedClusters(nodeIds, adjacencyList) {
|
||||||
|
const visited = new Set();
|
||||||
|
const clusters = [];
|
||||||
|
|
||||||
|
for (const nodeId of nodeIds) {
|
||||||
|
if (!visited.has(nodeId)) {
|
||||||
|
const cluster = [];
|
||||||
|
const stack = [nodeId];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const current = stack.pop();
|
||||||
|
if (!visited.has(current)) {
|
||||||
|
visited.add(current);
|
||||||
|
cluster.push(current);
|
||||||
|
|
||||||
|
// Add unvisited neighbors within the unreachable set
|
||||||
|
for (const neighbor of (adjacencyList[current] || [])) {
|
||||||
|
if (nodeIds.includes(neighbor) && !visited.has(neighbor)) {
|
||||||
|
stack.push(neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cluster.length > 0) {
|
||||||
|
clusters.push(cluster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ENHANCED: Get comprehensive graph statistics with forensic information
|
||||||
|
* Updates the existing getStatistics() method
|
||||||
*/
|
*/
|
||||||
getStatistics() {
|
getStatistics() {
|
||||||
return {
|
const basicStats = {
|
||||||
nodeCount: this.nodes.length,
|
nodeCount: this.nodes.length,
|
||||||
edgeCount: this.edges.length,
|
edgeCount: this.edges.length,
|
||||||
largeEntityMembersHidden: this.largeEntityMembers.size
|
largeEntityMembersHidden: this.largeEntityMembers.size
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add forensic statistics
|
||||||
|
const visibleNodes = this.nodes.get({ filter: node => !node.hidden });
|
||||||
|
const hiddenNodes = this.nodes.get({ filter: node => node.hidden });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...basicStats,
|
||||||
|
forensicStatistics: {
|
||||||
|
visibleNodes: visibleNodes.length,
|
||||||
|
hiddenNodes: hiddenNodes.length,
|
||||||
|
trueRoots: this.trueRootIds.size,
|
||||||
|
integrityStatus: visibleNodes.length > 0 && this.trueRootIds.size > 0 ? 'INTACT' : 'COMPROMISED'
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
computeTrueRoots() {
|
computeTrueRoots() {
|
||||||
@ -1069,75 +1212,185 @@ class GraphManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ENHANCED: Apply filters using consolidated reachability analysis
|
||||||
|
* Replaces the existing applyAllFilters() method
|
||||||
|
*/
|
||||||
applyAllFilters() {
|
applyAllFilters() {
|
||||||
console.log("Applying all filters (robust orphan detection)...");
|
console.log("Applying filters with enhanced reachability analysis...");
|
||||||
if (this.nodes.length === 0) return;
|
if (this.nodes.length === 0) return;
|
||||||
|
|
||||||
// 1. Get filter criteria from the UI
|
// Get filter criteria from UI
|
||||||
const hiddenNodeTypes = new Set();
|
const excludedNodeTypes = new Set();
|
||||||
this.filterPanel.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => {
|
this.filterPanel?.querySelectorAll('input[data-filter-type="node"]:not(:checked)').forEach(cb => {
|
||||||
hiddenNodeTypes.add(cb.value);
|
excludedNodeTypes.add(cb.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hiddenEdgeTypes = new Set();
|
const excludedEdgeTypes = new Set();
|
||||||
this.filterPanel.querySelectorAll('input[data-filter-type="edge"]:not(:checked)').forEach(cb => {
|
this.filterPanel?.querySelectorAll('input[data-filter-type="edge"]:not(:checked)').forEach(cb => {
|
||||||
hiddenEdgeTypes.add(cb.value);
|
excludedEdgeTypes.add(cb.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Build adjacency list for the visible part of the graph
|
// Perform comprehensive analysis
|
||||||
const adj = {};
|
const analysis = this.analyzeGraphReachability(new Set(), excludedEdgeTypes, excludedNodeTypes);
|
||||||
this.nodes.getIds().forEach(id => adj[id] = []);
|
|
||||||
this.edges.forEach(edge => {
|
// Apply visibility updates
|
||||||
if (!hiddenEdgeTypes.has(edge.metadata.relationship_type)) {
|
|
||||||
adj[edge.from].push(edge.to);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Traverse from "true roots" to find all reachable nodes
|
|
||||||
const reachableNodes = new Set();
|
|
||||||
const queue = [];
|
|
||||||
|
|
||||||
// Start the traversal from true roots that aren't hidden by type
|
|
||||||
this.trueRootIds.forEach(rootId => {
|
|
||||||
const node = this.nodes.get(rootId);
|
|
||||||
if (node && !hiddenNodeTypes.has(node.type)) {
|
|
||||||
if (!reachableNodes.has(rootId)) {
|
|
||||||
queue.push(rootId);
|
|
||||||
reachableNodes.add(rootId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let head = 0;
|
|
||||||
while (head < queue.length) {
|
|
||||||
const u = queue[head++];
|
|
||||||
|
|
||||||
for (const v of (adj[u] || [])) {
|
|
||||||
if (!reachableNodes.has(v)) {
|
|
||||||
const node = this.nodes.get(v);
|
|
||||||
if (node && !hiddenNodeTypes.has(node.type)) {
|
|
||||||
reachableNodes.add(v);
|
|
||||||
queue.push(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Create final node and edge visibility updates
|
|
||||||
const nodeUpdates = this.nodes.map(node => ({
|
const nodeUpdates = this.nodes.map(node => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
hidden: !reachableNodes.has(node.id)
|
hidden: !analysis.reachableNodes.has(node.id)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeUpdates = this.edges.map(edge => ({
|
const edgeUpdates = this.edges.map(edge => ({
|
||||||
id: edge.id,
|
id: edge.id,
|
||||||
hidden: hiddenEdgeTypes.has(edge.metadata.relationship_type) || !reachableNodes.has(edge.from) || !reachableNodes.has(edge.to)
|
hidden: excludedEdgeTypes.has(edge.metadata?.relationship_type || '') ||
|
||||||
|
!analysis.reachableNodes.has(edge.from) ||
|
||||||
|
!analysis.reachableNodes.has(edge.to)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.nodes.update(nodeUpdates);
|
this.nodes.update(nodeUpdates);
|
||||||
this.edges.update(edgeUpdates);
|
this.edges.update(edgeUpdates);
|
||||||
|
|
||||||
console.log(`Filters applied. Reachable nodes: ${reachableNodes.size}`);
|
console.log(`Enhanced filters applied. Visible nodes: ${analysis.reachableNodes.size}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ENHANCED: Hide node with forensic integrity using reachability analysis
|
||||||
|
* Replaces the existing hideNodeAndOrphans() method
|
||||||
|
*/
|
||||||
|
hideNodeWithReachabilityAnalysis(nodeId) {
|
||||||
|
console.log(`Hiding node ${nodeId} with reachability analysis...`);
|
||||||
|
|
||||||
|
// Simulate hiding this node and analyze impact
|
||||||
|
const excludedNodes = new Set([nodeId]);
|
||||||
|
const analysis = this.analyzeGraphReachability(excludedNodes);
|
||||||
|
|
||||||
|
// Nodes that will become unreachable (should be hidden)
|
||||||
|
const nodesToHide = [nodeId, ...Array.from(analysis.unreachableNodes)];
|
||||||
|
|
||||||
|
// Store history for potential revert
|
||||||
|
const historyData = {
|
||||||
|
nodeIds: nodesToHide,
|
||||||
|
operation: 'hide_with_reachability',
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply hiding with forensic documentation
|
||||||
|
const updates = nodesToHide.map(id => ({
|
||||||
|
id: id,
|
||||||
|
hidden: true,
|
||||||
|
forensicNote: `Hidden due to reachability analysis from ${nodeId}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.nodes.update(updates);
|
||||||
|
this.addToHistory('hide', historyData);
|
||||||
|
|
||||||
|
console.log(`Forensic hide operation: ${nodesToHide.length} nodes hidden`, {
|
||||||
|
originalTarget: nodeId,
|
||||||
|
cascadeNodes: nodesToHide.length - 1,
|
||||||
|
isolatedClusters: analysis.isolatedClusters.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenNodes: nodesToHide,
|
||||||
|
isolatedClusters: analysis.isolatedClusters
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ENHANCED: Delete node with forensic integrity using reachability analysis
|
||||||
|
* Replaces the existing deleteNodeAndOrphans() method
|
||||||
|
*/
|
||||||
|
async deleteNodeWithReachabilityAnalysis(nodeId) {
|
||||||
|
console.log(`Deleting node ${nodeId} with reachability analysis...`);
|
||||||
|
|
||||||
|
// Simulate deletion and analyze impact
|
||||||
|
const excludedNodes = new Set([nodeId]);
|
||||||
|
const analysis = this.analyzeGraphReachability(excludedNodes);
|
||||||
|
|
||||||
|
// Nodes that will become unreachable (should be deleted)
|
||||||
|
const nodesToDelete = [nodeId, ...Array.from(analysis.unreachableNodes)];
|
||||||
|
|
||||||
|
// Collect forensic data before deletion
|
||||||
|
const historyData = {
|
||||||
|
nodes: nodesToDelete.map(id => this.nodes.get(id)).filter(Boolean),
|
||||||
|
edges: [],
|
||||||
|
operation: 'delete_with_reachability',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
forensicAnalysis: {
|
||||||
|
originalTarget: nodeId,
|
||||||
|
cascadeNodes: nodesToDelete.length - 1,
|
||||||
|
isolatedClusters: analysis.isolatedClusters.length,
|
||||||
|
clusterSizes: analysis.isolatedClusters.map(cluster => cluster.length)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect affected edges
|
||||||
|
nodesToDelete.forEach(id => {
|
||||||
|
const connectedEdgeIds = this.network.getConnectedEdges(id);
|
||||||
|
const edges = this.edges.get(connectedEdgeIds);
|
||||||
|
historyData.edges.push(...edges);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove duplicates from edges
|
||||||
|
historyData.edges = Array.from(new Map(historyData.edges.map(e => [e.id, e])).values());
|
||||||
|
|
||||||
|
// Perform backend deletion with forensic logging
|
||||||
|
let operationFailed = false;
|
||||||
|
|
||||||
|
for (const targetNodeId of nodesToDelete) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/graph/node/${targetNodeId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
forensicContext: {
|
||||||
|
operation: 'reachability_cascade_delete',
|
||||||
|
originalTarget: nodeId,
|
||||||
|
analysisTimestamp: historyData.timestamp
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`Backend deletion failed for node ${targetNodeId}:`, result.error);
|
||||||
|
operationFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Node ${targetNodeId} deleted from backend with forensic context`);
|
||||||
|
this.nodes.remove({ id: targetNodeId });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`API error during deletion of node ${targetNodeId}:`, error);
|
||||||
|
operationFailed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle operation results
|
||||||
|
if (!operationFailed) {
|
||||||
|
this.addToHistory('delete', historyData);
|
||||||
|
console.log(`Forensic delete operation completed:`, historyData.forensicAnalysis);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deletedNodes: nodesToDelete,
|
||||||
|
forensicAnalysis: historyData.forensicAnalysis
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Revert UI changes if backend operations failed
|
||||||
|
console.log("Reverting UI changes due to backend failure");
|
||||||
|
this.nodes.add(historyData.nodes);
|
||||||
|
this.edges.add(historyData.edges);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Backend deletion failed, UI reverted"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1207,29 +1460,34 @@ class GraphManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform action from the context menu
|
* UPDATED: Enhanced context menu actions using new methods
|
||||||
* @param {string} action - The action to perform ('hide' or 'delete')
|
* Updates the existing performContextMenuAction() method
|
||||||
* @param {string} nodeId - The ID of the node
|
|
||||||
*/
|
*/
|
||||||
performContextMenuAction(action, nodeId) {
|
performContextMenuAction(action, nodeId) {
|
||||||
console.log('Performing action:', action, 'on node:', nodeId);
|
console.log('Performing enhanced action:', action, 'on node:', nodeId);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'focus':
|
case 'focus':
|
||||||
this.focusOnNode(nodeId);
|
this.focusOnNode(nodeId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'hide':
|
case 'hide':
|
||||||
this.hideNodeAndOrphans(nodeId);
|
// Use enhanced method with reachability analysis
|
||||||
|
this.hideNodeWithReachabilityAnalysis(nodeId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
this.deleteNodeAndOrphans(nodeId);
|
// Use enhanced method with reachability analysis
|
||||||
|
this.deleteNodeWithReachabilityAnalysis(nodeId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'details':
|
case 'details':
|
||||||
const node = this.nodes.get(nodeId);
|
const node = this.nodes.get(nodeId);
|
||||||
if (node) {
|
if (node) {
|
||||||
this.showNodeDetails(node);
|
this.showNodeDetails(node);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('Unknown action:', action);
|
console.warn('Unknown action:', action);
|
||||||
}
|
}
|
||||||
@ -1289,121 +1547,6 @@ class GraphManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide a node and recursively hide any neighbors that become disconnected.
|
|
||||||
* @param {string} nodeId - The ID of the node to start hiding from.
|
|
||||||
*/
|
|
||||||
hideNodeAndOrphans(nodeId) {
|
|
||||||
const historyData = { nodeIds: [] };
|
|
||||||
const queue = [nodeId];
|
|
||||||
const visited = new Set([nodeId]);
|
|
||||||
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const currentId = queue.shift();
|
|
||||||
const node = this.nodes.get(currentId);
|
|
||||||
if (!node || node.hidden) continue;
|
|
||||||
|
|
||||||
// 1. Hide the current node and add to history
|
|
||||||
this.nodes.update({ id: currentId, hidden: true });
|
|
||||||
historyData.nodeIds.push(currentId);
|
|
||||||
|
|
||||||
// 2. Check its neighbors
|
|
||||||
const neighbors = this.network.getConnectedNodes(currentId);
|
|
||||||
for (const neighborId of neighbors) {
|
|
||||||
if (visited.has(neighborId)) continue;
|
|
||||||
|
|
||||||
const connectedEdges = this.network.getConnectedEdges(neighborId);
|
|
||||||
let hasVisibleEdge = false;
|
|
||||||
// 3. See if the neighbor still has any visible connections
|
|
||||||
for (const edgeId of connectedEdges) {
|
|
||||||
const edge = this.edges.get(edgeId);
|
|
||||||
const sourceNode = this.nodes.get(edge.from);
|
|
||||||
const targetNode = this.nodes.get(edge.to);
|
|
||||||
if ((sourceNode && !sourceNode.hidden) && (targetNode && !targetNode.hidden)) {
|
|
||||||
hasVisibleEdge = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. If no visible connections, add to queue to be hidden
|
|
||||||
if (!hasVisibleEdge) {
|
|
||||||
queue.push(neighborId);
|
|
||||||
visited.add(neighborId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (historyData.nodeIds.length > 0) {
|
|
||||||
this.addToHistory('hide', historyData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a node and recursively delete any neighbors that become disconnected.
|
|
||||||
* @param {string} nodeId - The ID of the node to start deleting from.
|
|
||||||
*/
|
|
||||||
async deleteNodeAndOrphans(nodeId) {
|
|
||||||
const deletionQueue = [nodeId];
|
|
||||||
const processedForDeletion = new Set([nodeId]);
|
|
||||||
const historyData = { nodes: [], edges: [] };
|
|
||||||
let operationFailed = false;
|
|
||||||
|
|
||||||
while (deletionQueue.length > 0) {
|
|
||||||
const currentId = deletionQueue.shift();
|
|
||||||
const node = this.nodes.get(currentId);
|
|
||||||
if (!node) continue;
|
|
||||||
|
|
||||||
const neighbors = this.network.getConnectedNodes(currentId);
|
|
||||||
const connectedEdgeIds = this.network.getConnectedEdges(currentId);
|
|
||||||
const edges = this.edges.get(connectedEdgeIds);
|
|
||||||
|
|
||||||
// Store state for potential revert
|
|
||||||
historyData.nodes.push(node);
|
|
||||||
historyData.edges.push(...edges);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/graph/node/${currentId}`, { method: 'DELETE' });
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
console.error(`Failed to delete node ${currentId} from backend:`, result.error);
|
|
||||||
operationFailed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Node ${currentId} deleted from backend.`);
|
|
||||||
this.nodes.remove({ id: currentId }); // Remove from view
|
|
||||||
|
|
||||||
// Check if former neighbors are now orphans
|
|
||||||
neighbors.forEach(neighborId => {
|
|
||||||
if (!processedForDeletion.has(neighborId) && this.nodes.get(neighborId)) {
|
|
||||||
if (this.network.getConnectedEdges(neighborId).length === 0) {
|
|
||||||
deletionQueue.push(neighborId);
|
|
||||||
processedForDeletion.add(neighborId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error during node deletion API call:', error);
|
|
||||||
operationFailed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to history only if the entire operation was successful
|
|
||||||
if (!operationFailed && historyData.nodes.length > 0) {
|
|
||||||
// Ensure edges in history are unique
|
|
||||||
historyData.edges = Array.from(new Map(historyData.edges.map(e => [e.id, e])).values());
|
|
||||||
this.addToHistory('delete', historyData);
|
|
||||||
} else if (operationFailed) {
|
|
||||||
console.log("Reverting UI changes due to failed delete operation.");
|
|
||||||
// If any part of the chain failed, restore the UI to its original state
|
|
||||||
this.nodes.add(historyData.nodes);
|
|
||||||
this.edges.add(historyData.edges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unhide all hidden nodes
|
* Unhide all hidden nodes
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user