Compare commits
1 Commits
main
...
5850931614
| Author | SHA1 | Date | |
|---|---|---|---|
| 5850931614 |
@@ -99,12 +99,10 @@ class TopologyGraphComponent extends Component {
|
|||||||
.map(([k, v]) => `<span class=\"label-chip\">${this.escapeHtml(String(k))}: ${this.escapeHtml(String(v))}</span>`)
|
.map(([k, v]) => `<span class=\"label-chip\">${this.escapeHtml(String(k))}: ${this.escapeHtml(String(v))}</span>`)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
// Add hint for non-primary nodes
|
// Add hint for node interactions
|
||||||
let hint = '';
|
let hint = '<div style="margin-top: 8px; font-size: 11px; color: rgba(255, 255, 255, 0.7); text-align: center;">💡 Click to set as primary & toggle view</div>';
|
||||||
if (nodeData && !nodeData.isPrimary) {
|
if (nodeData && nodeData.isPrimary) {
|
||||||
hint = '<div style="margin-top: 8px; font-size: 11px; color: rgba(255, 255, 255, 0.7); text-align: center;">💡 Click to switch to primary & view details</div>';
|
hint = '<div style="margin-top: 8px; font-size: 11px; color: rgba(255, 215, 0, 0.9); text-align: center;">⭐ Primary Node - Click to toggle view</div>';
|
||||||
} else if (nodeData && nodeData.isPrimary) {
|
|
||||||
hint = '<div style="margin-top: 8px; font-size: 11px; color: rgba(255, 215, 0, 0.9); text-align: center;">⭐ Primary Node - Click to view details</div>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltipEl.innerHTML = `<div class=\"member-labels\">${chips}</div>${hint}`;
|
this.tooltipEl.innerHTML = `<div class=\"member-labels\">${chips}</div>${hint}`;
|
||||||
@@ -622,30 +620,19 @@ class TopologyGraphComponent extends Component {
|
|||||||
.attr('stroke', d => d.isPrimary ? '#FFD700' : '#fff')
|
.attr('stroke', d => d.isPrimary ? '#FFD700' : '#fff')
|
||||||
.attr('stroke-width', d => d.isPrimary ? 3 : 2);
|
.attr('stroke-width', d => d.isPrimary ? 3 : 2);
|
||||||
|
|
||||||
// Status indicator
|
// Status indicator - removed
|
||||||
nodeEnter.append('circle')
|
|
||||||
.attr('r', 3)
|
|
||||||
.attr('fill', d => this.getStatusIndicatorColor(d.status))
|
|
||||||
.attr('cx', -8)
|
|
||||||
.attr('cy', -8);
|
|
||||||
|
|
||||||
// Primary node badge
|
// Primary node badge
|
||||||
const primaryBadge = nodeEnter.filter(d => d.isPrimary)
|
const primaryBadge = nodeEnter.filter(d => d.isPrimary)
|
||||||
.append('g')
|
.append('g')
|
||||||
.attr('class', 'primary-badge')
|
.attr('class', 'primary-badge')
|
||||||
.attr('transform', 'translate(8, -8)');
|
.attr('transform', 'translate(0, 0)');
|
||||||
|
|
||||||
primaryBadge.append('circle')
|
|
||||||
.attr('r', 8)
|
|
||||||
.attr('fill', '#FFD700')
|
|
||||||
.attr('stroke', '#FFF')
|
|
||||||
.attr('stroke-width', 1);
|
|
||||||
|
|
||||||
primaryBadge.append('text')
|
primaryBadge.append('text')
|
||||||
.text('P')
|
.text('P')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('dominant-baseline', 'central')
|
.attr('dominant-baseline', 'central')
|
||||||
.attr('font-size', '10px')
|
.attr('font-size', '12px')
|
||||||
.attr('font-weight', 'bold')
|
.attr('font-weight', 'bold')
|
||||||
.attr('fill', '#000');
|
.attr('fill', '#000');
|
||||||
|
|
||||||
@@ -701,10 +688,7 @@ class TopologyGraphComponent extends Component {
|
|||||||
.attr('stroke', d => d.isPrimary ? '#FFD700' : '#fff')
|
.attr('stroke', d => d.isPrimary ? '#FFD700' : '#fff')
|
||||||
.attr('stroke-width', d => d.isPrimary ? 3 : 2);
|
.attr('stroke-width', d => d.isPrimary ? 3 : 2);
|
||||||
|
|
||||||
nodeMerge.select('circle:nth-child(2)')
|
// Update status indicator - removed
|
||||||
.transition()
|
|
||||||
.duration(300)
|
|
||||||
.attr('fill', d => this.getStatusIndicatorColor(d.status));
|
|
||||||
|
|
||||||
nodeMerge.select('.hostname-text')
|
nodeMerge.select('.hostname-text')
|
||||||
.text(d => d.hostname.length > 15 ? d.hostname.substring(0, 15) + '...' : d.hostname);
|
.text(d => d.hostname.length > 15 ? d.hostname.substring(0, 15) + '...' : d.hostname);
|
||||||
@@ -735,19 +719,13 @@ class TopologyGraphComponent extends Component {
|
|||||||
if (nodeGroup.select('.primary-badge').empty()) {
|
if (nodeGroup.select('.primary-badge').empty()) {
|
||||||
const badge = nodeGroup.append('g')
|
const badge = nodeGroup.append('g')
|
||||||
.attr('class', 'primary-badge')
|
.attr('class', 'primary-badge')
|
||||||
.attr('transform', 'translate(8, -8)');
|
.attr('transform', 'translate(0, 0)');
|
||||||
|
|
||||||
badge.append('circle')
|
|
||||||
.attr('r', 8)
|
|
||||||
.attr('fill', '#FFD700')
|
|
||||||
.attr('stroke', '#FFF')
|
|
||||||
.attr('stroke-width', 1);
|
|
||||||
|
|
||||||
badge.append('text')
|
badge.append('text')
|
||||||
.text('P')
|
.text('P')
|
||||||
.attr('text-anchor', 'middle')
|
.attr('text-anchor', 'middle')
|
||||||
.attr('dominant-baseline', 'central')
|
.attr('dominant-baseline', 'central')
|
||||||
.attr('font-size', '10px')
|
.attr('font-size', '12px')
|
||||||
.attr('font-weight', 'bold')
|
.attr('font-weight', 'bold')
|
||||||
.attr('fill', '#000');
|
.attr('fill', '#000');
|
||||||
}
|
}
|
||||||
@@ -769,7 +747,33 @@ class TopologyGraphComponent extends Component {
|
|||||||
.on('click', async (event, d) => {
|
.on('click', async (event, d) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// Always open drawer/details for the clicked node
|
// Check if we're toggling back to mesh (clicking same center node in star mode)
|
||||||
|
const currentMode = this.viewModel.get('topologyMode');
|
||||||
|
const currentCenter = this.viewModel.get('starCenterNode');
|
||||||
|
const togglingToMesh = currentMode === 'star' && currentCenter === d.ip;
|
||||||
|
|
||||||
|
// Toggle topology mode - switch between mesh and star
|
||||||
|
await this.viewModel.toggleTopologyMode(d.ip);
|
||||||
|
|
||||||
|
// Also switch to this node as primary (if not already)
|
||||||
|
if (!d.isPrimary) {
|
||||||
|
try {
|
||||||
|
logger.info(`TopologyGraphComponent: Switching primary to ${d.ip}`);
|
||||||
|
await this.viewModel.switchToPrimaryNode(d.ip);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('TopologyGraphComponent: Failed to switch primary:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open or close drawer based on mode toggle
|
||||||
|
if (togglingToMesh) {
|
||||||
|
// Switching back to mesh - close drawer
|
||||||
|
this.viewModel.clearSelection();
|
||||||
|
if (this.isDesktop()) {
|
||||||
|
this.closeDrawer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Switching to star or changing center - open drawer
|
||||||
this.viewModel.selectNode(d.id);
|
this.viewModel.selectNode(d.id);
|
||||||
this.updateSelection(d.id);
|
this.updateSelection(d.id);
|
||||||
if (this.isDesktop()) {
|
if (this.isDesktop()) {
|
||||||
@@ -777,52 +781,6 @@ class TopologyGraphComponent extends Component {
|
|||||||
} else {
|
} else {
|
||||||
this.showMemberCardOverlay(d);
|
this.showMemberCardOverlay(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If clicking on non-primary node, also switch it to primary
|
|
||||||
if (!d.isPrimary) {
|
|
||||||
try {
|
|
||||||
// Visual feedback - highlight the clicked node
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('stroke-width', 4)
|
|
||||||
.attr('stroke', '#FFD700');
|
|
||||||
|
|
||||||
logger.info(`TopologyGraphComponent: Switching primary to ${d.ip}`);
|
|
||||||
|
|
||||||
// Switch to this node as primary
|
|
||||||
await this.viewModel.switchToPrimaryNode(d.ip);
|
|
||||||
|
|
||||||
// Show success feedback briefly
|
|
||||||
this.showTooltip({
|
|
||||||
...d,
|
|
||||||
hostname: `✓ Switched to ${d.hostname}`
|
|
||||||
}, event.pageX, event.pageY);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.hideTooltip();
|
|
||||||
}, 1500);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('TopologyGraphComponent: Failed to switch primary:', error);
|
|
||||||
|
|
||||||
// Show error feedback
|
|
||||||
this.showTooltip({
|
|
||||||
...d,
|
|
||||||
hostname: `✗ Failed: ${error.message}`
|
|
||||||
}, event.pageX, event.pageY);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.hideTooltip();
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// Revert visual feedback
|
|
||||||
d3.select(event.currentTarget).select('circle')
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr('stroke-width', 2)
|
|
||||||
.attr('stroke', '#fff');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('mouseover', (event, d) => {
|
.on('mouseover', (event, d) => {
|
||||||
@@ -843,6 +801,20 @@ class TopologyGraphComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateLinkLabels(svgGroup, links) {
|
updateLinkLabels(svgGroup, links) {
|
||||||
|
const topologyMode = this.viewModel.get('topologyMode') || 'mesh';
|
||||||
|
|
||||||
|
// Only show latency labels in star mode
|
||||||
|
if (topologyMode !== 'star') {
|
||||||
|
// Clear any existing labels in mesh mode
|
||||||
|
const linkLabelGroup = svgGroup.select('.link-label-group');
|
||||||
|
if (!linkLabelGroup.empty()) {
|
||||||
|
linkLabelGroup.selectAll('text').remove();
|
||||||
|
}
|
||||||
|
this.linkLabelSelection = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Star mode - show latency labels
|
||||||
// Get or create link label group
|
// Get or create link label group
|
||||||
let linkLabelGroup = svgGroup.select('.link-label-group');
|
let linkLabelGroup = svgGroup.select('.link-label-group');
|
||||||
if (linkLabelGroup.empty()) {
|
if (linkLabelGroup.empty()) {
|
||||||
@@ -1005,10 +977,8 @@ class TopologyGraphComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addLegend(svgGroup) {
|
addLegend(svgGroup) {
|
||||||
// Only add legend if it doesn't exist
|
// Remove existing legend to recreate with current mode
|
||||||
if (!svgGroup.select('.legend-group').empty()) {
|
svgGroup.select('.legend-group').remove();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const legend = svgGroup.append('g')
|
const legend = svgGroup.append('g')
|
||||||
.attr('class', 'legend-group graph-element')
|
.attr('class', 'legend-group graph-element')
|
||||||
@@ -1125,16 +1095,20 @@ class TopologyGraphComponent extends Component {
|
|||||||
.attr('font-size', '12px')
|
.attr('font-size', '12px')
|
||||||
.attr('fill', '#ecf0f1');
|
.attr('fill', '#ecf0f1');
|
||||||
|
|
||||||
// Star topology info
|
// Topology mode info
|
||||||
|
const topologyMode = this.viewModel.get('topologyMode') || 'mesh';
|
||||||
|
const modeText = topologyMode === 'mesh' ? 'Full Mesh' : 'Star Topology';
|
||||||
|
const modeDesc = topologyMode === 'mesh' ? '(All to All)' : '(Center to Others)';
|
||||||
|
|
||||||
topologyLegend.append('text')
|
topologyLegend.append('text')
|
||||||
.text('Star Topology')
|
.text(modeText)
|
||||||
.attr('x', 0)
|
.attr('x', 0)
|
||||||
.attr('y', 50)
|
.attr('y', 50)
|
||||||
.attr('font-size', '12px')
|
.attr('font-size', '12px')
|
||||||
.attr('fill', '#ecf0f1');
|
.attr('fill', '#ecf0f1');
|
||||||
|
|
||||||
topologyLegend.append('text')
|
topologyLegend.append('text')
|
||||||
.text('(Primary to Members)')
|
.text(modeDesc)
|
||||||
.attr('x', 0)
|
.attr('x', 0)
|
||||||
.attr('y', 65)
|
.attr('y', 65)
|
||||||
.attr('font-size', '10px')
|
.attr('font-size', '10px')
|
||||||
@@ -1288,7 +1262,9 @@ class TopologyGraphComponent extends Component {
|
|||||||
// Get or create animation group inside the main transformed group
|
// Get or create animation group inside the main transformed group
|
||||||
let animGroup = mainGroup.select('.discovery-animation-group');
|
let animGroup = mainGroup.select('.discovery-animation-group');
|
||||||
if (animGroup.empty()) {
|
if (animGroup.empty()) {
|
||||||
animGroup = mainGroup.append('g').attr('class', 'discovery-animation-group');
|
animGroup = mainGroup.append('g')
|
||||||
|
.attr('class', 'discovery-animation-group')
|
||||||
|
.style('pointer-events', 'none'); // Make entire animation group non-interactive
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit a dot to each other node
|
// Emit a dot to each other node
|
||||||
@@ -1300,20 +1276,38 @@ class TopologyGraphComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animateDiscoveryDot(animGroup, sourceNode, targetNode) {
|
animateDiscoveryDot(animGroup, sourceNode, targetNode) {
|
||||||
// Create a small circle at the source node
|
// Calculate spawn position outside the node boundary
|
||||||
|
const nodeRadius = this.getNodeRadius(sourceNode.status);
|
||||||
|
const spawnDistance = nodeRadius + 8; // Spawn 8px outside the node
|
||||||
|
|
||||||
|
// Calculate direction from source to target
|
||||||
|
const dx = targetNode.x - sourceNode.x;
|
||||||
|
const dy = targetNode.y - sourceNode.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// Normalize direction and spawn outside the node
|
||||||
|
const normalizedX = dx / distance;
|
||||||
|
const normalizedY = dy / distance;
|
||||||
|
const spawnX = sourceNode.x + normalizedX * spawnDistance;
|
||||||
|
const spawnY = sourceNode.y + normalizedY * spawnDistance;
|
||||||
|
|
||||||
|
// Create a small circle outside the source node
|
||||||
const dot = animGroup.append('circle')
|
const dot = animGroup.append('circle')
|
||||||
.attr('class', 'discovery-dot')
|
.attr('class', 'discovery-dot')
|
||||||
.attr('r', 6)
|
.attr('r', 6)
|
||||||
.attr('cx', sourceNode.x)
|
.attr('cx', spawnX)
|
||||||
.attr('cy', sourceNode.y)
|
.attr('cy', spawnY)
|
||||||
.attr('fill', '#FFD700')
|
.attr('fill', '#FFD700')
|
||||||
.attr('opacity', 1)
|
.attr('opacity', 1)
|
||||||
.attr('stroke', '#FFF')
|
.attr('stroke', '#FFF')
|
||||||
.attr('stroke-width', 2);
|
.attr('stroke-width', 2)
|
||||||
|
.style('pointer-events', 'none'); // Make dots non-interactive
|
||||||
|
|
||||||
// Trigger response dot early (after 70% of the journey)
|
// Trigger response dot early (after 70% of the journey) - only if target node is active
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (targetNode.status && targetNode.status.toUpperCase() === 'ACTIVE') {
|
||||||
this.animateResponseDot(animGroup, targetNode, sourceNode);
|
this.animateResponseDot(animGroup, targetNode, sourceNode);
|
||||||
|
}
|
||||||
}, 1050); // 1500ms * 0.7 = 1050ms
|
}, 1050); // 1500ms * 0.7 = 1050ms
|
||||||
|
|
||||||
// Animate the dot to the target node
|
// Animate the dot to the target node
|
||||||
@@ -1327,23 +1321,51 @@ class TopologyGraphComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
animateResponseDot(animGroup, sourceNode, targetNode) {
|
animateResponseDot(animGroup, sourceNode, targetNode) {
|
||||||
// Create a response dot at the target (now source) node
|
// Calculate spawn position outside the target node boundary
|
||||||
|
const nodeRadius = this.getNodeRadius(sourceNode.status);
|
||||||
|
const spawnDistance = nodeRadius + 8; // Spawn 8px outside the node
|
||||||
|
|
||||||
|
// Calculate direction from target back to original source
|
||||||
|
const dx = targetNode.x - sourceNode.x;
|
||||||
|
const dy = targetNode.y - sourceNode.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
// Normalize direction and spawn outside the node
|
||||||
|
const normalizedX = dx / distance;
|
||||||
|
const normalizedY = dy / distance;
|
||||||
|
const spawnX = sourceNode.x + normalizedX * spawnDistance;
|
||||||
|
const spawnY = sourceNode.y + normalizedY * spawnDistance;
|
||||||
|
|
||||||
|
// Calculate target position outside the original source node
|
||||||
|
const targetNodeRadius = this.getNodeRadius(targetNode.status);
|
||||||
|
const targetDistance = targetNodeRadius + 8; // Stop 8px outside the target node
|
||||||
|
|
||||||
|
// Calculate direction from source back to target
|
||||||
|
const targetDx = sourceNode.x - targetNode.x;
|
||||||
|
const targetDy = sourceNode.y - targetNode.y;
|
||||||
|
const targetNormalizedX = targetDx / distance;
|
||||||
|
const targetNormalizedY = targetDy / distance;
|
||||||
|
const targetX = targetNode.x + targetNormalizedX * targetDistance;
|
||||||
|
const targetY = targetNode.y + targetNormalizedY * targetDistance;
|
||||||
|
|
||||||
|
// Create a response dot outside the target (now source) node
|
||||||
const responseDot = animGroup.append('circle')
|
const responseDot = animGroup.append('circle')
|
||||||
.attr('class', 'discovery-dot-response')
|
.attr('class', 'discovery-dot-response')
|
||||||
.attr('r', 5)
|
.attr('r', 5)
|
||||||
.attr('cx', sourceNode.x)
|
.attr('cx', spawnX)
|
||||||
.attr('cy', sourceNode.y)
|
.attr('cy', spawnY)
|
||||||
.attr('fill', '#4CAF50')
|
.attr('fill', '#2196F3')
|
||||||
.attr('opacity', 1)
|
.attr('opacity', 1)
|
||||||
.attr('stroke', '#FFF')
|
.attr('stroke', '#FFF')
|
||||||
.attr('stroke-width', 2);
|
.attr('stroke-width', 2)
|
||||||
|
.style('pointer-events', 'none'); // Make dots non-interactive
|
||||||
|
|
||||||
// Animate back to the original source
|
// Animate back to outside the original source node
|
||||||
responseDot.transition()
|
responseDot.transition()
|
||||||
.duration(1000)
|
.duration(1000)
|
||||||
.ease(d3.easeCubicInOut)
|
.ease(d3.easeCubicInOut)
|
||||||
.attr('cx', targetNode.x)
|
.attr('cx', targetX)
|
||||||
.attr('cy', targetNode.y)
|
.attr('cy', targetY)
|
||||||
.attr('opacity', 0)
|
.attr('opacity', 0)
|
||||||
.remove();
|
.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -584,7 +584,9 @@ class TopologyViewModel extends ViewModel {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
lastUpdateTime: null,
|
lastUpdateTime: null,
|
||||||
selectedNode: null
|
selectedNode: null,
|
||||||
|
topologyMode: 'mesh', // 'mesh' or 'star'
|
||||||
|
starCenterNode: null // IP of the center node in star mode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,7 +694,7 @@ class TopologyViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build enhanced graph data with actual node connections
|
// Build enhanced graph data with actual node connections
|
||||||
// Creates a star topology with the primary node at the center
|
// Creates either a mesh topology (all nodes connected) or star topology (center node to all others)
|
||||||
async buildEnhancedGraphData(members, primaryNode) {
|
async buildEnhancedGraphData(members, primaryNode) {
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
const links = [];
|
const links = [];
|
||||||
@@ -730,32 +732,53 @@ class TopologyViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build links - create a star topology with primary node at center
|
// Build links based on topology mode
|
||||||
// Only create links from the primary node to each member
|
const topologyMode = this.get('topologyMode') || 'mesh';
|
||||||
// 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)`);
|
if (topologyMode === 'mesh') {
|
||||||
|
// Full mesh - connect all nodes to all other nodes
|
||||||
|
logger.debug('TopologyViewModel: Creating full mesh topology');
|
||||||
|
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];
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
source: primaryNode,
|
source: sourceNode.id,
|
||||||
|
target: targetNode.id,
|
||||||
|
latency: this.estimateLatency(sourceNode, targetNode),
|
||||||
|
sourceNode: sourceNode,
|
||||||
|
targetNode: targetNode,
|
||||||
|
bidirectional: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug(`TopologyViewModel: Created ${links.length} links in mesh topology`);
|
||||||
|
} else if (topologyMode === 'star') {
|
||||||
|
// Star topology - center node connects to all others
|
||||||
|
const centerNode = this.get('starCenterNode') || primaryNode;
|
||||||
|
|
||||||
|
if (centerNode) {
|
||||||
|
logger.debug(`TopologyViewModel: Creating star topology with center ${centerNode}`);
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.ip !== centerNode) {
|
||||||
|
const member = members.find(m => m.ip === node.ip);
|
||||||
|
const latency = member?.latency || this.estimateLatency(node, { ip: centerNode });
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
source: centerNode,
|
||||||
target: node.id,
|
target: node.id,
|
||||||
latency: latency,
|
latency: latency,
|
||||||
sourceNode: nodes.find(n => n.ip === primaryNode),
|
sourceNode: nodes.find(n => n.ip === centerNode),
|
||||||
targetNode: node,
|
targetNode: node,
|
||||||
bidirectional: false // Primary -> Member is directional
|
bidirectional: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
logger.debug(`TopologyViewModel: Created ${links.length} links from primary node`);
|
logger.debug(`TopologyViewModel: Created ${links.length} links from center node`);
|
||||||
} else {
|
} else {
|
||||||
logger.warn('TopologyViewModel: No primary node specified, cannot create links');
|
logger.warn('TopologyViewModel: No center node specified for star topology');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nodes, links };
|
return { nodes, links };
|
||||||
@@ -803,6 +826,36 @@ class TopologyViewModel extends ViewModel {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle topology mode or set star mode with specific center node
|
||||||
|
async setTopologyMode(mode, centerNodeIp = null) {
|
||||||
|
logger.debug(`TopologyViewModel: Setting topology mode to ${mode}`, centerNodeIp ? `with center ${centerNodeIp}` : '');
|
||||||
|
|
||||||
|
this.setMultiple({
|
||||||
|
topologyMode: mode,
|
||||||
|
starCenterNode: centerNodeIp
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rebuild the graph with new topology
|
||||||
|
await this.updateNetworkTopology();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle between mesh and star modes
|
||||||
|
async toggleTopologyMode(nodeIp) {
|
||||||
|
const currentMode = this.get('topologyMode');
|
||||||
|
const currentCenter = this.get('starCenterNode');
|
||||||
|
|
||||||
|
if (currentMode === 'mesh') {
|
||||||
|
// Switch to star mode with this node as center
|
||||||
|
await this.setTopologyMode('star', nodeIp);
|
||||||
|
} else if (currentMode === 'star' && currentCenter === nodeIp) {
|
||||||
|
// Clicking same center node - switch back to mesh
|
||||||
|
await this.setTopologyMode('mesh', null);
|
||||||
|
} else {
|
||||||
|
// Clicking different node - change star center
|
||||||
|
await this.setTopologyMode('star', nodeIp);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitoring View Model for cluster resource monitoring
|
// Monitoring View Model for cluster resource monitoring
|
||||||
|
|||||||
Reference in New Issue
Block a user