feat: primary node switching in topology graph

This commit is contained in:
2025-10-24 22:30:57 +02:00
parent fa6777a042
commit b4bd459d27
6 changed files with 427 additions and 46 deletions

View File

@@ -601,13 +601,14 @@ class TopologyViewModel extends ViewModel {
// Update topology from WebSocket data
if (data.members && Array.isArray(data.members)) {
logger.debug(`TopologyViewModel: Updating topology with ${data.members.length} members`);
logger.debug(`TopologyViewModel: Updating topology with ${data.members.length} members, primary: ${data.primaryNode}`);
// Build enhanced graph data from updated members
this.buildEnhancedGraphData(data.members).then(({ nodes, links }) => {
// Build enhanced graph data from updated members with primary node info
this.buildEnhancedGraphData(data.members, data.primaryNode).then(({ nodes, links }) => {
this.batchUpdate({
nodes: nodes,
links: links,
primaryNode: data.primaryNode,
lastUpdateTime: data.timestamp || new Date().toISOString()
});
}).catch(error => {
@@ -658,14 +659,24 @@ class TopologyViewModel extends ViewModel {
const response = await window.apiClient.getClusterMembers();
logger.debug('TopologyViewModel: Got cluster members response:', response);
// Get discovery info to find the primary node
const discoveryInfo = await window.apiClient.getDiscoveryInfo();
logger.debug('TopologyViewModel: Got discovery info:', discoveryInfo);
const members = response.members || [];
const primaryNode = discoveryInfo.primaryNode || null;
logger.debug(`TopologyViewModel: Building graph with ${members.length} members, primary: ${primaryNode}`);
// Build enhanced graph data with actual node connections
const { nodes, links } = await this.buildEnhancedGraphData(members);
const { nodes, links } = await this.buildEnhancedGraphData(members, primaryNode);
logger.debug(`TopologyViewModel: Built graph with ${nodes.length} nodes and ${links.length} links`);
this.batchUpdate({
nodes: nodes,
links: links,
primaryNode: primaryNode,
lastUpdateTime: new Date().toISOString()
});
@@ -679,7 +690,8 @@ class TopologyViewModel extends ViewModel {
}
// Build enhanced graph data with actual node connections
async buildEnhancedGraphData(members) {
// Creates a star topology with the primary node at the center
async buildEnhancedGraphData(members, primaryNode) {
const nodes = [];
const links = [];
@@ -691,6 +703,7 @@ class TopologyViewModel extends ViewModel {
members.forEach((member, index) => {
if (member && member.ip) {
const existingNode = existingNodeMap.get(member.ip);
const isPrimary = member.ip === primaryNode;
nodes.push({
id: member.ip,
@@ -698,6 +711,7 @@ class TopologyViewModel extends ViewModel {
ip: member.ip,
status: member.status || 'UNKNOWN',
latency: member.latency || 0,
isPrimary: isPrimary,
// Preserve both legacy 'resources' and preferred 'labels'
labels: (member.labels && typeof member.labels === 'object') ? member.labels : (member.resources || {}),
resources: member.resources || {},
@@ -714,28 +728,32 @@ class TopologyViewModel extends ViewModel {
}
});
// 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
});
}
// Build links - create a star topology with primary node at center
// Only create links from the primary node to each member
// The cluster data comes from the primary, so it only knows about its direct connections
if (primaryNode) {
logger.debug(`TopologyViewModel: Creating star topology with primary ${primaryNode}`);
nodes.forEach(node => {
// Create a link from primary to each non-primary node
if (node.ip !== primaryNode) {
const member = members.find(m => m.ip === node.ip);
const latency = member?.latency || this.estimateLatency(node, { ip: primaryNode });
logger.debug(`TopologyViewModel: Creating link from ${primaryNode} to ${node.ip} (latency: ${latency}ms)`);
links.push({
source: primaryNode,
target: node.id,
latency: latency,
sourceNode: nodes.find(n => n.ip === primaryNode),
targetNode: node,
bidirectional: false // Primary -> Member is directional
});
}
});
logger.debug(`TopologyViewModel: Created ${links.length} links from primary node`);
} else {
logger.warn('TopologyViewModel: No primary node specified, cannot create links');
}
return { nodes, links };
@@ -758,6 +776,31 @@ class TopologyViewModel extends ViewModel {
clearSelection() {
this.set('selectedNode', null);
}
// Switch to a specific node as the new primary
async switchToPrimaryNode(nodeIp) {
try {
logger.debug(`TopologyViewModel: Switching primary node to ${nodeIp}`);
const result = await window.apiClient.setPrimaryNode(nodeIp);
if (result.success) {
logger.info(`TopologyViewModel: Successfully switched primary to ${nodeIp}`);
// Update topology after a short delay to allow backend to update
setTimeout(async () => {
logger.debug('TopologyViewModel: Refreshing topology after primary switch...');
await this.updateNetworkTopology();
}, 1000);
return result;
} else {
throw new Error(result.message || 'Failed to switch primary node');
}
} catch (error) {
logger.error('TopologyViewModel: Failed to switch primary node:', error);
throw error;
}
}
}
// Monitoring View Model for cluster resource monitoring