diff --git a/public/scripts/components/TopologyGraphComponent.js b/public/scripts/components/TopologyGraphComponent.js index 18a5c51..bec4aa4 100644 --- a/public/scripts/components/TopologyGraphComponent.js +++ b/public/scripts/components/TopologyGraphComponent.js @@ -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 diff --git a/public/scripts/view-models.js b/public/scripts/view-models.js index 3f49f6d..f487282 100644 --- a/public/scripts/view-models.js +++ b/public/scripts/view-models.js @@ -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 + }); } });