diff --git a/public/components.js b/public/components.js index cc73745..50ee30a 100644 --- a/public/components.js +++ b/public/components.js @@ -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 = '
No cluster members found
'; } + 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 ` +
+
+
+

Member Details

+
+ +
+ +
+
+
+
+
${member.hostname || 'Unknown Device'}
+
${member.ip || 'No IP'}
+
+ ${statusIcon} ${statusText} +
+
+ Latency: + ${member.latency ? member.latency + 'ms' : 'N/A'} +
+ ${member.labels && Object.keys(member.labels).length ? ` +
+ ${Object.entries(member.labels).map(([key, value]) => `${key}: ${value}`).join('')} +
+ ` : ''} +
+
+
+
Loading detailed information...
+
+
+
+
+ `; + } + + 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 = ` +
+ Error loading node details:
+ ${error.message} +
+ `; + } + } + + 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 = '
Loading detailed information...
'; + } + } + + } \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 9f06172..5310f13 100644 --- a/public/styles.css +++ b/public/styles.css @@ -2383,6 +2383,201 @@ p { height: 100%; } +/* Member Card Overlay Styles */ +.member-card-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.member-card-overlay.visible { + opacity: 1; + visibility: visible; +} + +.member-overlay-content { + background: linear-gradient(135deg, #1c2a38 0%, #283746 50%, #1a252f 100%); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); + max-width: 800px; + width: 90%; + max-height: 90vh; + overflow: hidden; + transform: scale(0.9) translateY(20px); + transition: all 0.3s ease; +} + +.member-card-overlay.visible .member-overlay-content { + transform: scale(1) translateY(0); +} + +.member-overlay-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 24px 24px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.member-overlay-title h3 { + margin: 0 0 8px 0; + font-size: 1.5rem; + font-weight: 600; + color: #ecf0f1; +} + +.member-overlay-subtitle { + font-size: 1rem; + color: rgba(255, 255, 255, 0.7); + font-family: 'Courier New', monospace; +} + +.member-overlay-close { + background: none; + border: none; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + padding: 8px; + border-radius: 8px; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.member-overlay-close:hover { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.9); +} + +.member-overlay-close svg { + width: 20px; + height: 20px; +} + +.member-overlay-body { + padding: 24px; +} + +.member-overlay-section { + margin-bottom: 24px; +} + +/* Member card container within overlay */ +.member-overlay-body { + padding: 0; + overflow: auto; + max-height: calc(90vh - 120px); /* Account for header */ +} + +/* Ensure member cards render properly in overlay */ +.member-overlay-body .member-card { + margin: 0; + border-radius: 0; + border: none; + box-shadow: none; + background: transparent; +} + +.member-overlay-body .member-card .member-header { + padding: 20px 24px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.member-overlay-body .member-card .member-details { + padding: 20px 24px; +} + +/* Hide expand icon in overlay since card is always expanded */ +.member-overlay-body .member-card .expand-icon { + display: none; +} + +/* Ensure expanded state is visually clear */ +.member-overlay-body .member-card.expanded .member-details { + display: block; +} + +/* Highlight animation for member cards */ +.member-card.highlighted { + animation: highlight-pulse 2s ease-in-out; +} + +@keyframes highlight-pulse { + 0%, 100% { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + } + 50% { + box-shadow: 0 8px 32px rgba(59, 130, 246, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3); + } +} + +/* Responsive design for overlay */ +@media (max-width: 768px) { + .member-overlay-content { + width: 95%; + max-width: none; + margin: 20px; + } + + .member-overlay-header { + padding: 20px 20px 12px; + } + + .member-overlay-body { + padding: 20px; + } + + .member-overlay-actions { + flex-direction: column; + } + + .member-overlay-title h3 { + font-size: 1.3rem; + } +} + +/* Test page styles */ +.test-section { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 24px; + margin: 24px 0; +} + +.test-section h2 { + color: #ecf0f1; + margin-top: 0; +} + +#test-overlay-btn { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + color: white; + border: none; + padding: 12px 24px; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +#test-overlay-btn:hover { + background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); + transform: translateY(-1px); +} + /* Loading and error states */ .loading, .error, .no-data { display: flex; diff --git a/public/test-member-overlay.html b/public/test-member-overlay.html new file mode 100644 index 0000000..e18c82e --- /dev/null +++ b/public/test-member-overlay.html @@ -0,0 +1,58 @@ + + + + + + Test Member Card Overlay + + + + +
+

Test Member Card Overlay

+ +
+

Test Data

+ +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/public/test-topology-overlay.html b/public/test-topology-overlay.html new file mode 100644 index 0000000..abca027 --- /dev/null +++ b/public/test-topology-overlay.html @@ -0,0 +1,63 @@ + + + + + + Test Topology with Member Card Overlay + + + + +
+

Test Topology with Member Card Overlay

+ +
+

Topology View with Clickable Nodes

+

Click on any node in the topology to see the member card overlay.

+ +
+ +
+
+
Loading network topology...
+
+
+ +
+
+ + + + + + + \ No newline at end of file