feat: live topology view through websocket updates
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user