Compare commits

..

1 Commits

Author SHA1 Message Date
936679f274 feat: live topology view through websocket updates 2025-10-23 21:27:31 +02:00
2 changed files with 93 additions and 38 deletions

View File

@@ -1025,13 +1025,22 @@ class TopologyGraphComponent extends Component {
.attr('stroke', d => d.id === selectedNodeId ? '#2196F3' : '#fff'); .attr('stroke', d => d.id === selectedNodeId ? '#2196F3' : '#fff');
} }
// NOTE: This method is deprecated and should not be used
// The topology graph is now entirely websocket-driven
// Refresh button was removed and all updates come from websocket events
handleRefresh() { handleRefresh() {
logger.warn('TopologyGraphComponent: handleRefresh called - this method is deprecated'); logger.debug('TopologyGraphComponent: handleRefresh called');
logger.warn('TopologyGraphComponent: Topology updates should come from websocket events only');
// No-op - do not make API calls if (!this.isInitialized) {
logger.debug('TopologyGraphComponent: Component not yet initialized, ensuring initialization...');
this.ensureInitialized().then(() => {
// Refresh after initialization
this.viewModel.updateNetworkTopology();
}).catch(error => {
logger.error('TopologyGraphComponent: Failed to initialize for refresh:', error);
});
return;
}
logger.debug('TopologyGraphComponent: Calling updateNetworkTopology...');
this.viewModel.updateNetworkTopology();
} }
handleLoadingState(isLoading) { handleLoadingState(isLoading) {

View File

@@ -621,22 +621,27 @@ class TopologyViewModel extends ViewModel {
// Listen for node discovery events // Listen for node discovery events
window.wsClient.on('nodeDiscovery', (data) => { window.wsClient.on('nodeDiscovery', (data) => {
logger.debug('TopologyViewModel: Received WebSocket node discovery event:', data); logger.debug('TopologyViewModel: Received WebSocket node discovery event:', data);
// Node discovery events are logged but no action needed
// The subsequent clusterUpdate event will provide the updated member list
// and update the topology through the websocket data flow
if (data.action === 'discovered') { if (data.action === 'discovered') {
logger.debug('TopologyViewModel: Node discovered, waiting for clusterUpdate event'); // A new node was discovered - trigger a topology update
setTimeout(() => {
this.updateNetworkTopology();
}, 500);
} else if (data.action === 'stale') { } else if (data.action === 'stale') {
logger.debug('TopologyViewModel: Node became stale, waiting for clusterUpdate event'); // A node became stale - trigger a topology update
setTimeout(() => {
this.updateNetworkTopology();
}, 500);
} }
}); });
// Listen for connection status changes // Listen for connection status changes
window.wsClient.on('connected', () => { window.wsClient.on('connected', () => {
logger.debug('TopologyViewModel: WebSocket connected'); logger.debug('TopologyViewModel: WebSocket connected');
// Connection restored - the server will send a clusterUpdate event shortly // Trigger an immediate update when connection is restored
// No need to make an API call, just wait for the websocket data setTimeout(() => {
this.updateNetworkTopology();
}, 1000);
}); });
window.wsClient.on('disconnected', () => { window.wsClient.on('disconnected', () => {
@@ -645,11 +650,9 @@ class TopologyViewModel extends ViewModel {
} }
// Update network topology data // Update network topology data
// NOTE: This method makes an API call and should only be used for initial load
// All subsequent updates should come from websocket events (clusterUpdate)
async updateNetworkTopology() { async updateNetworkTopology() {
try { try {
logger.debug('TopologyViewModel: updateNetworkTopology called (API call)'); logger.debug('TopologyViewModel: updateNetworkTopology called');
this.set('isLoading', true); this.set('isLoading', true);
this.set('error', null); this.set('error', null);
@@ -682,6 +685,7 @@ class TopologyViewModel extends ViewModel {
async buildEnhancedGraphData(members) { async buildEnhancedGraphData(members) {
const nodes = []; const nodes = [];
const links = []; const links = [];
const nodeConnections = new Map();
// Get existing nodes to preserve their positions // Get existing nodes to preserve their positions
const existingNodes = this.get('nodes') || []; const existingNodes = this.get('nodes') || [];
@@ -714,27 +718,69 @@ class TopologyViewModel extends ViewModel {
} }
}); });
// Build links - create a mesh topology from the members data // Try to get cluster members from each node to build actual connections
// All connections are inferred from the cluster membership for (const node of nodes) {
// No additional API calls needed - all data comes from websocket updates try {
for (let i = 0; i < nodes.length; i++) { const nodeResponse = await window.apiClient.getClusterMembersFromNode(node.ip);
for (let j = i + 1; j < nodes.length; j++) { if (nodeResponse && nodeResponse.members) {
const sourceNode = nodes[i]; nodeConnections.set(node.ip, nodeResponse.members);
const targetNode = nodes[j]; }
} catch (error) {
// Use the latency from the member data if available, otherwise estimate console.warn(`Failed to get cluster members from node ${node.ip}:`, error);
const sourceMember = members.find(m => m.ip === sourceNode.ip); // Continue with other nodes
const targetMember = members.find(m => m.ip === targetNode.ip); }
const latency = targetMember?.latency || sourceMember?.latency || this.estimateLatency(sourceNode, targetNode); }
links.push({ // Build links based on actual connections
source: sourceNode.id, for (const [sourceIp, sourceMembers] of nodeConnections) {
target: targetNode.id, for (const targetMember of sourceMembers) {
latency: latency, if (targetMember.ip && targetMember.ip !== sourceIp) {
sourceNode: sourceNode, // Check if we already have this link
targetNode: targetNode, const existingLink = links.find(link =>
bidirectional: true (link.source === sourceIp && link.target === targetMember.ip) ||
}); (link.source === targetMember.ip && link.target === sourceIp)
);
if (!existingLink) {
const sourceNode = nodes.find(n => n.id === sourceIp);
const targetNode = nodes.find(n => n.id === targetMember.ip);
if (sourceNode && targetNode) {
const latency = targetMember.latency || this.estimateLatency(sourceNode, targetNode);
links.push({
source: sourceIp,
target: targetMember.ip,
latency: latency,
sourceNode: sourceNode,
targetNode: targetNode,
bidirectional: true
});
}
}
}
}
}
// If no actual connections found, create a basic mesh
if (links.length === 0) {
logger.debug('TopologyViewModel: No actual connections found, creating basic mesh');
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const sourceNode = nodes[i];
const targetNode = nodes[j];
const estimatedLatency = this.estimateLatency(sourceNode, targetNode);
links.push({
source: sourceNode.id,
target: targetNode.id,
latency: estimatedLatency,
sourceNode: sourceNode,
targetNode: targetNode,
bidirectional: true
});
}
} }
} }