From 41be660d9438968c9002c38333707a4b4d333637 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Mon, 15 Sep 2025 21:36:23 +0200 Subject: [PATCH] feat: add labels --- .../components/NodeDetailsComponent.js | 11 ++- .../components/TopologyGraphComponent.js | 98 ++++++++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/public/scripts/components/NodeDetailsComponent.js b/public/scripts/components/NodeDetailsComponent.js index fe12e6e..b716481 100644 --- a/public/scripts/components/NodeDetailsComponent.js +++ b/public/scripts/components/NodeDetailsComponent.js @@ -86,8 +86,17 @@ class NodeDetailsComponent extends Component { // Use persisted active tab from the view model, default to 'status' const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status'; logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab); - + + // Build labels bar (above tabs) + const labelsObj = (nodeStatus && nodeStatus.labels) ? nodeStatus.labels : null; + const labelsBar = (labelsObj && Object.keys(labelsObj).length) + ? `
${Object.entries(labelsObj) + .map(([k, v]) => `${this.escapeHtml(String(k))}: ${this.escapeHtml(String(v))}`) + .join('')}
` + : ''; + const html = ` + ${labelsBar}
diff --git a/public/scripts/components/TopologyGraphComponent.js b/public/scripts/components/TopologyGraphComponent.js index cf6cc4e..c4b7c7e 100644 --- a/public/scripts/components/TopologyGraphComponent.js +++ b/public/scripts/components/TopologyGraphComponent.js @@ -9,6 +9,96 @@ class TopologyGraphComponent extends Component { this.width = 0; // Will be set dynamically based on container size this.height = 0; // Will be set dynamically based on container size this.isInitialized = false; + + // Drawer state for desktop reuse (same pattern as ClusterMembersComponent) + this.detailsDrawer = null; + this.detailsDrawerContent = null; + this.detailsDrawerBackdrop = null; + this.activeDrawerComponent = null; + } + + // Determine desktop threshold + isDesktop() { + try { return window && window.innerWidth >= 1024; } catch (_) { return false; } + } + + ensureDrawer() { + if (this.detailsDrawer) return; + // Backdrop + this.detailsDrawerBackdrop = document.createElement('div'); + this.detailsDrawerBackdrop.className = 'details-drawer-backdrop'; + document.body.appendChild(this.detailsDrawerBackdrop); + + // Drawer + this.detailsDrawer = document.createElement('div'); + this.detailsDrawer.className = 'details-drawer'; + + const header = document.createElement('div'); + header.className = 'details-drawer-header'; + header.innerHTML = ` +
Node Details
+ + `; + this.detailsDrawer.appendChild(header); + + this.detailsDrawerContent = document.createElement('div'); + this.detailsDrawerContent.className = 'details-drawer-content'; + this.detailsDrawer.appendChild(this.detailsDrawerContent); + + document.body.appendChild(this.detailsDrawer); + + const close = () => this.closeDrawer(); + header.querySelector('.drawer-close').addEventListener('click', close); + this.detailsDrawerBackdrop.addEventListener('click', close); + document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(); }); + } + + openDrawerForNode(nodeData) { + this.ensureDrawer(); + + // Title from hostname or IP + try { + const displayName = nodeData.hostname || nodeData.ip || 'Node Details'; + const titleEl = this.detailsDrawer.querySelector('.drawer-title'); + if (titleEl) titleEl.textContent = displayName; + } catch (_) {} + + // Clear previous component + if (this.activeDrawerComponent && typeof this.activeDrawerComponent.unmount === 'function') { + try { this.activeDrawerComponent.unmount(); } catch (_) {} + } + this.detailsDrawerContent.innerHTML = '
Loading detailed information...
'; + + // Mount NodeDetailsComponent + const nodeDetailsVM = new NodeDetailsViewModel(); + const nodeDetailsComponent = new NodeDetailsComponent(this.detailsDrawerContent, nodeDetailsVM, this.eventBus); + this.activeDrawerComponent = nodeDetailsComponent; + + const ip = nodeData.ip || nodeData.id; + nodeDetailsVM.loadNodeDetails(ip).then(() => { + nodeDetailsComponent.mount(); + }).catch((error) => { + logger.error('Failed to load node details (topology drawer):', error); + this.detailsDrawerContent.innerHTML = ` +
+ Error loading node details:
+ ${this.escapeHtml(error.message)} +
+ `; + }); + + // Open + this.detailsDrawer.classList.add('open'); + this.detailsDrawerBackdrop.classList.add('visible'); + } + + closeDrawer() { + if (this.detailsDrawer) this.detailsDrawer.classList.remove('open'); + if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible'); } updateDimensions(container) { @@ -345,7 +435,13 @@ class TopologyGraphComponent extends Component { node.on('click', (event, d) => { this.viewModel.selectNode(d.id); this.updateSelection(d.id); - this.showMemberCardOverlay(d); + if (this.isDesktop()) { + // Desktop: open slide-in drawer, reuse NodeDetailsComponent + this.openDrawerForNode(d); + } else { + // Mobile/low-res: keep existing overlay + this.showMemberCardOverlay(d); + } }); node.on('mouseover', (event, d) => {