feat: node_discovered indicator in graph
This commit is contained in:
@@ -230,6 +230,7 @@ class TopologyGraphComponent extends Component {
|
|||||||
this.subscribeToProperty('isLoading', this.handleLoadingState.bind(this));
|
this.subscribeToProperty('isLoading', this.handleLoadingState.bind(this));
|
||||||
this.subscribeToProperty('error', this.handleError.bind(this));
|
this.subscribeToProperty('error', this.handleError.bind(this));
|
||||||
this.subscribeToProperty('selectedNode', this.updateSelection.bind(this));
|
this.subscribeToProperty('selectedNode', this.updateSelection.bind(this));
|
||||||
|
this.subscribeToProperty('discoveryEvent', this.handleDiscoveryEvent.bind(this));
|
||||||
} else {
|
} else {
|
||||||
// Component not yet initialized, store for later
|
// Component not yet initialized, store for later
|
||||||
logger.debug('TopologyGraphComponent: Component not initialized, storing pending subscriptions');
|
logger.debug('TopologyGraphComponent: Component not initialized, storing pending subscriptions');
|
||||||
@@ -238,7 +239,8 @@ class TopologyGraphComponent extends Component {
|
|||||||
['links', this.renderGraph.bind(this)],
|
['links', this.renderGraph.bind(this)],
|
||||||
['isLoading', this.handleLoadingState.bind(this)],
|
['isLoading', this.handleLoadingState.bind(this)],
|
||||||
['error', this.handleError.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');
|
.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
|
// NOTE: This method is deprecated and should not be used
|
||||||
// The topology graph is now entirely websocket-driven
|
// The topology graph is now entirely websocket-driven
|
||||||
// Refresh button was removed and all updates come from websocket events
|
// Refresh button was removed and all updates come from websocket events
|
||||||
|
|||||||
@@ -623,13 +623,15 @@ class TopologyViewModel extends ViewModel {
|
|||||||
window.wsClient.on('nodeDiscovery', (data) => {
|
window.wsClient.on('nodeDiscovery', (data) => {
|
||||||
logger.debug('TopologyViewModel: Received WebSocket node discovery event:', data);
|
logger.debug('TopologyViewModel: Received WebSocket node discovery event:', data);
|
||||||
|
|
||||||
// Node discovery events are logged but no action needed
|
// Trigger animation for 'discovered' or 'active' actions (node is alive)
|
||||||
// The subsequent clusterUpdate event will provide the updated member list
|
// Skip animation for 'stale' or 'inactive' actions
|
||||||
// and update the topology through the websocket data flow
|
if (data.action === 'discovered' || data.action === 'active') {
|
||||||
if (data.action === 'discovered') {
|
// Emit discovery animation event
|
||||||
logger.debug('TopologyViewModel: Node discovered, waiting for clusterUpdate event');
|
this.set('discoveryEvent', {
|
||||||
} else if (data.action === 'stale') {
|
nodeIp: data.nodeIp,
|
||||||
logger.debug('TopologyViewModel: Node became stale, waiting for clusterUpdate event');
|
timestamp: data.timestamp || new Date().toISOString(),
|
||||||
|
action: data.action
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user