feat: node_discovered indicator in graph

This commit is contained in:
2025-10-25 08:30:02 +02:00
parent b4bd459d27
commit 74473cbc26
2 changed files with 99 additions and 8 deletions

View File

@@ -230,6 +230,7 @@ class TopologyGraphComponent extends Component {
this.subscribeToProperty('isLoading', this.handleLoadingState.bind(this));
this.subscribeToProperty('error', this.handleError.bind(this));
this.subscribeToProperty('selectedNode', this.updateSelection.bind(this));
this.subscribeToProperty('discoveryEvent', this.handleDiscoveryEvent.bind(this));
} else {
// Component not yet initialized, store for later
logger.debug('TopologyGraphComponent: Component not initialized, storing pending subscriptions');
@@ -238,7 +239,8 @@ class TopologyGraphComponent extends Component {
['links', this.renderGraph.bind(this)],
['isLoading', this.handleLoadingState.bind(this)],
['error', this.handleError.bind(this)],
['selectedNode', this.updateSelection.bind(this)]
['selectedNode', this.updateSelection.bind(this)],
['discoveryEvent', this.handleDiscoveryEvent.bind(this)]
];
}
}
@@ -1259,6 +1261,93 @@ class TopologyGraphComponent extends Component {
.attr('stroke', d => d.id === selectedNodeId ? '#2196F3' : '#fff');
}
handleDiscoveryEvent(discoveryEvent) {
if (!discoveryEvent || !discoveryEvent.nodeIp) {
return;
}
// Emit discovery dots from the discovered node to all other nodes
this.emitDiscoveryDots(discoveryEvent.nodeIp);
}
emitDiscoveryDots(sourceNodeIp) {
if (!this.svg || !this.isInitialized) {
return;
}
const nodes = this.viewModel.get('nodes') || [];
const sourceNode = nodes.find(n => n.ip === sourceNodeIp);
if (!sourceNode || !sourceNode.x || !sourceNode.y) {
return;
}
// Get the main graph group (that has the transform applied)
const mainGroup = this.svg.select('g');
// Get or create animation group inside the main transformed group
let animGroup = mainGroup.select('.discovery-animation-group');
if (animGroup.empty()) {
animGroup = mainGroup.append('g').attr('class', 'discovery-animation-group');
}
// Emit a dot to each other node
nodes.forEach(targetNode => {
if (targetNode.ip !== sourceNodeIp && targetNode.x && targetNode.y) {
this.animateDiscoveryDot(animGroup, sourceNode, targetNode);
}
});
}
animateDiscoveryDot(animGroup, sourceNode, targetNode) {
// Create a small circle at the source node
const dot = animGroup.append('circle')
.attr('class', 'discovery-dot')
.attr('r', 6)
.attr('cx', sourceNode.x)
.attr('cy', sourceNode.y)
.attr('fill', '#FFD700')
.attr('opacity', 1)
.attr('stroke', '#FFF')
.attr('stroke-width', 2);
// Trigger response dot early (after 70% of the journey)
setTimeout(() => {
this.animateResponseDot(animGroup, targetNode, sourceNode);
}, 1050); // 1500ms * 0.7 = 1050ms
// Animate the dot to the target node
dot.transition()
.duration(1500)
.ease(d3.easeCubicInOut)
.attr('cx', targetNode.x)
.attr('cy', targetNode.y)
.attr('opacity', 0)
.remove();
}
animateResponseDot(animGroup, sourceNode, targetNode) {
// Create a response dot at the target (now source) node
const responseDot = animGroup.append('circle')
.attr('class', 'discovery-dot-response')
.attr('r', 5)
.attr('cx', sourceNode.x)
.attr('cy', sourceNode.y)
.attr('fill', '#4CAF50')
.attr('opacity', 1)
.attr('stroke', '#FFF')
.attr('stroke-width', 2);
// Animate back to the original source
responseDot.transition()
.duration(1000)
.ease(d3.easeCubicInOut)
.attr('cx', targetNode.x)
.attr('cy', targetNode.y)
.attr('opacity', 0)
.remove();
}
// 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

View File

@@ -623,13 +623,15 @@ class TopologyViewModel extends ViewModel {
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');
// Trigger animation for 'discovered' or 'active' actions (node is alive)
// Skip animation for 'stale' or 'inactive' actions
if (data.action === 'discovered' || data.action === 'active') {
// Emit discovery animation event
this.set('discoveryEvent', {
nodeIp: data.nodeIp,
timestamp: data.timestamp || new Date().toISOString(),
action: data.action
});
}
});