feat: live topology view through websocket updates

This commit is contained in:
2025-10-23 20:36:07 +02:00
parent ce836e7636
commit fa6777a042
4 changed files with 909 additions and 250 deletions

View File

@@ -588,10 +588,68 @@ class TopologyViewModel extends ViewModel {
});
}
// Set up WebSocket event listeners for real-time topology updates
setupWebSocketListeners() {
if (!window.wsClient) {
logger.warn('TopologyViewModel: WebSocket client not available');
return;
}
// Listen for cluster updates
window.wsClient.on('clusterUpdate', (data) => {
logger.debug('TopologyViewModel: Received WebSocket cluster update:', data);
// Update topology from WebSocket data
if (data.members && Array.isArray(data.members)) {
logger.debug(`TopologyViewModel: Updating topology with ${data.members.length} members`);
// Build enhanced graph data from updated members
this.buildEnhancedGraphData(data.members).then(({ nodes, links }) => {
this.batchUpdate({
nodes: nodes,
links: links,
lastUpdateTime: data.timestamp || new Date().toISOString()
});
}).catch(error => {
logger.error('TopologyViewModel: Failed to build graph data from websocket update:', error);
});
} else {
logger.warn('TopologyViewModel: Received cluster update but no valid members array:', data);
}
});
// Listen for node discovery events
window.wsClient.on('nodeDiscovery', (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') {
logger.debug('TopologyViewModel: Node discovered, waiting for clusterUpdate event');
} else if (data.action === 'stale') {
logger.debug('TopologyViewModel: Node became stale, waiting for clusterUpdate event');
}
});
// Listen for connection status changes
window.wsClient.on('connected', () => {
logger.debug('TopologyViewModel: WebSocket connected');
// Connection restored - the server will send a clusterUpdate event shortly
// No need to make an API call, just wait for the websocket data
});
window.wsClient.on('disconnected', () => {
logger.debug('TopologyViewModel: WebSocket disconnected');
});
}
// 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() {
try {
logger.debug('TopologyViewModel: updateNetworkTopology called');
logger.debug('TopologyViewModel: updateNetworkTopology called (API call)');
this.set('isLoading', true);
this.set('error', null);
@@ -624,11 +682,16 @@ class TopologyViewModel extends ViewModel {
async buildEnhancedGraphData(members) {
const nodes = [];
const links = [];
const nodeConnections = new Map();
// Get existing nodes to preserve their positions
const existingNodes = this.get('nodes') || [];
const existingNodeMap = new Map(existingNodes.map(n => [n.id, n]));
// Create nodes from members
members.forEach((member, index) => {
if (member && member.ip) {
const existingNode = existingNodeMap.get(member.ip);
nodes.push({
id: member.ip,
hostname: member.hostname || member.ip,
@@ -638,75 +701,40 @@ class TopologyViewModel extends ViewModel {
// Preserve both legacy 'resources' and preferred 'labels'
labels: (member.labels && typeof member.labels === 'object') ? member.labels : (member.resources || {}),
resources: member.resources || {},
x: Math.random() * 1200 + 100, // Better spacing for 1400px width
y: Math.random() * 800 + 100 // Better spacing for 1000px height
// Preserve existing position if node already exists, otherwise assign random position
x: existingNode ? existingNode.x : Math.random() * 1200 + 100,
y: existingNode ? existingNode.y : Math.random() * 800 + 100,
// Preserve velocity if it exists (for D3 simulation)
vx: existingNode ? existingNode.vx : undefined,
vy: existingNode ? existingNode.vy : undefined,
// Preserve fixed position if it was dragged
fx: existingNode ? existingNode.fx : undefined,
fy: existingNode ? existingNode.fy : undefined
});
}
});
// Try to get cluster members from each node to build actual connections
for (const node of nodes) {
try {
const nodeResponse = await window.apiClient.getClusterMembersFromNode(node.ip);
if (nodeResponse && nodeResponse.members) {
nodeConnections.set(node.ip, nodeResponse.members);
}
} catch (error) {
console.warn(`Failed to get cluster members from node ${node.ip}:`, error);
// Continue with other nodes
}
}
// Build links based on actual connections
for (const [sourceIp, sourceMembers] of nodeConnections) {
for (const targetMember of sourceMembers) {
if (targetMember.ip && targetMember.ip !== sourceIp) {
// Check if we already have this link
const existingLink = links.find(link =>
(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
});
}
// Build links - create a mesh topology from the members data
// All connections are inferred from the cluster membership
// No additional API calls needed - all data comes from websocket updates
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];
// Use the latency from the member data if available, otherwise estimate
const sourceMember = members.find(m => m.ip === sourceNode.ip);
const targetMember = members.find(m => m.ip === targetNode.ip);
const latency = targetMember?.latency || sourceMember?.latency || this.estimateLatency(sourceNode, targetNode);
links.push({
source: sourceNode.id,
target: targetNode.id,
latency: latency,
sourceNode: sourceNode,
targetNode: targetNode,
bidirectional: true
});
}
}