feat: add member details overlay to topology
This commit is contained in:
@@ -2579,10 +2579,13 @@ class TopologyGraphComponent extends Component {
|
||||
this.simulation.alpha(0.3).restart();
|
||||
}
|
||||
|
||||
// Add click handlers for node selection
|
||||
// Add click handlers for node selection and member card overlay
|
||||
node.on('click', (event, d) => {
|
||||
this.viewModel.selectNode(d.id);
|
||||
this.updateSelection(d.id);
|
||||
|
||||
// Show member card overlay
|
||||
this.showMemberCardOverlay(d);
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
@@ -2820,6 +2823,52 @@ class TopologyGraphComponent extends Component {
|
||||
container.innerHTML = '<div class="no-data"><div>No cluster members found</div></div>';
|
||||
}
|
||||
|
||||
showMemberCardOverlay(nodeData) {
|
||||
// Create overlay container if it doesn't exist
|
||||
let overlayContainer = document.getElementById('member-card-overlay');
|
||||
if (!overlayContainer) {
|
||||
overlayContainer = document.createElement('div');
|
||||
overlayContainer.id = 'member-card-overlay';
|
||||
overlayContainer.className = 'member-card-overlay';
|
||||
document.body.appendChild(overlayContainer);
|
||||
}
|
||||
|
||||
// Create and show the overlay component
|
||||
if (!this.memberOverlayComponent) {
|
||||
const overlayVM = new ViewModel();
|
||||
this.memberOverlayComponent = new MemberCardOverlayComponent(overlayContainer, overlayVM, this.eventBus);
|
||||
this.memberOverlayComponent.mount();
|
||||
}
|
||||
|
||||
// Convert node data to member data format
|
||||
const memberData = {
|
||||
ip: nodeData.ip,
|
||||
hostname: nodeData.hostname,
|
||||
status: this.normalizeStatus(nodeData.status),
|
||||
latency: nodeData.latency,
|
||||
labels: nodeData.resources || {}
|
||||
};
|
||||
|
||||
this.memberOverlayComponent.show(memberData);
|
||||
}
|
||||
|
||||
// Normalize status from topology format to member card format
|
||||
normalizeStatus(status) {
|
||||
if (!status) return 'unknown';
|
||||
|
||||
const normalized = status.toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'active':
|
||||
return 'active';
|
||||
case 'inactive':
|
||||
return 'inactive';
|
||||
case 'dead':
|
||||
return 'offline';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Override render method to display the graph
|
||||
render() {
|
||||
console.log('TopologyGraphComponent: render called');
|
||||
@@ -2839,4 +2888,175 @@ class TopologyGraphComponent extends Component {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Member Card Overlay Component for displaying member details in topology view
|
||||
class MemberCardOverlayComponent extends Component {
|
||||
constructor(container, viewModel, eventBus) {
|
||||
super(container, viewModel, eventBus);
|
||||
this.isVisible = false;
|
||||
this.currentMember = null;
|
||||
}
|
||||
|
||||
mount() {
|
||||
super.mount();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Close overlay when clicking outside or pressing escape
|
||||
this.addEventListener(document, 'click', (e) => {
|
||||
if (this.isVisible && !this.container.contains(e.target)) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener(document, 'keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.isVisible) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
show(memberData) {
|
||||
this.currentMember = memberData;
|
||||
this.isVisible = true;
|
||||
|
||||
const memberCardHTML = this.renderMemberCard(memberData);
|
||||
this.setHTML('', memberCardHTML);
|
||||
|
||||
// Add visible class for animation
|
||||
setTimeout(() => {
|
||||
this.container.classList.add('visible');
|
||||
}, 10);
|
||||
|
||||
// Setup member card interactions
|
||||
this.setupMemberCardInteractions();
|
||||
}
|
||||
|
||||
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.container.classList.remove('visible');
|
||||
this.currentMember = null;
|
||||
}
|
||||
|
||||
renderMemberCard(member) {
|
||||
const statusClass = member.status === 'active' ? 'status-online' :
|
||||
member.status === 'inactive' ? 'status-inactive' : 'status-offline';
|
||||
const statusText = member.status === 'active' ? 'Online' :
|
||||
member.status === 'inactive' ? 'Inactive' :
|
||||
member.status === 'offline' ? 'Offline' : 'Unknown';
|
||||
const statusIcon = member.status === 'active' ? '🟢' :
|
||||
member.status === 'inactive' ? '🟠' : '🔴';
|
||||
|
||||
return `
|
||||
<div class="member-overlay-content">
|
||||
<div class="member-overlay-header">
|
||||
<div class="member-overlay-title">
|
||||
<h3>Member Details</h3>
|
||||
</div>
|
||||
<button class="member-overlay-close" aria-label="Close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="member-overlay-body">
|
||||
<div class="member-card expanded" data-member-ip="${member.ip}">
|
||||
<div class="member-header">
|
||||
<div class="member-info">
|
||||
<div class="member-name">${member.hostname || 'Unknown Device'}</div>
|
||||
<div class="member-ip">${member.ip || 'No IP'}</div>
|
||||
<div class="member-status ${statusClass}">
|
||||
${statusIcon} ${statusText}
|
||||
</div>
|
||||
<div class="member-latency">
|
||||
<span class="latency-label">Latency:</span>
|
||||
<span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span>
|
||||
</div>
|
||||
${member.labels && Object.keys(member.labels).length ? `
|
||||
<div class="member-labels">
|
||||
${Object.entries(member.labels).map(([key, value]) => `<span class="label-chip">${key}: ${value}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-details">
|
||||
<div class="loading-details">Loading detailed information...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupMemberCardInteractions() {
|
||||
// Close button
|
||||
const closeBtn = this.findElement('.member-overlay-close');
|
||||
if (closeBtn) {
|
||||
this.addEventListener(closeBtn, 'click', () => {
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup member card expansion - automatically expand when shown
|
||||
setTimeout(async () => {
|
||||
const memberCard = this.findElement('.member-card');
|
||||
if (memberCard) {
|
||||
const memberDetails = memberCard.querySelector('.member-details');
|
||||
const memberIp = memberCard.dataset.memberIp;
|
||||
|
||||
// Automatically expand the card to show details
|
||||
await this.expandCard(memberCard, memberIp, memberDetails);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async expandCard(card, memberIp, memberDetails) {
|
||||
try {
|
||||
// Create node details view model and component
|
||||
const nodeDetailsVM = new NodeDetailsViewModel();
|
||||
const nodeDetailsComponent = new NodeDetailsComponent(memberDetails, nodeDetailsVM, this.eventBus);
|
||||
|
||||
// Load node details
|
||||
await nodeDetailsVM.loadNodeDetails(memberIp);
|
||||
|
||||
// Mount the component
|
||||
nodeDetailsComponent.mount();
|
||||
|
||||
// Update UI
|
||||
card.classList.add('expanded');
|
||||
const expandIcon = card.querySelector('.expand-icon');
|
||||
if (expandIcon) {
|
||||
expandIcon.classList.add('expanded');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to expand card:', error);
|
||||
memberDetails.innerHTML = `
|
||||
<div class="error">
|
||||
<strong>Error loading node details:</strong><br>
|
||||
${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
collapseCard(card, expandIcon) {
|
||||
card.classList.remove('expanded');
|
||||
if (expandIcon) {
|
||||
expandIcon.classList.remove('expanded');
|
||||
}
|
||||
|
||||
// Reset member details to loading state
|
||||
const memberDetails = card.querySelector('.member-details');
|
||||
if (memberDetails) {
|
||||
memberDetails.innerHTML = '<div class="loading-details">Loading detailed information...</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user