diff --git a/public/scripts/components/TopologyGraphComponent.js b/public/scripts/components/TopologyGraphComponent.js index c4b7c7e..6c7c58d 100644 --- a/public/scripts/components/TopologyGraphComponent.js +++ b/public/scripts/components/TopologyGraphComponent.js @@ -15,6 +15,9 @@ class TopologyGraphComponent extends Component { this.detailsDrawerContent = null; this.detailsDrawerBackdrop = null; this.activeDrawerComponent = null; + + // Tooltip for labels on hover + this.tooltipEl = null; } // Determine desktop threshold @@ -101,6 +104,52 @@ class TopologyGraphComponent extends Component { if (this.detailsDrawerBackdrop) this.detailsDrawerBackdrop.classList.remove('visible'); } + // Tooltip helpers + ensureTooltip() { + if (this.tooltipEl) return; + const el = document.createElement('div'); + el.className = 'topology-tooltip'; + document.body.appendChild(el); + this.tooltipEl = el; + } + + showTooltip(nodeData, pageX, pageY) { + this.ensureTooltip(); + const labels = (nodeData && nodeData.labels) ? nodeData.labels : ((nodeData && nodeData.resources) ? nodeData.resources : null); + if (!labels || Object.keys(labels).length === 0) { + this.hideTooltip(); + return; + } + const chips = Object.entries(labels) + .map(([k, v]) => `${this.escapeHtml(String(k))}: ${this.escapeHtml(String(v))}`) + .join(''); + this.tooltipEl.innerHTML = `
${chips}
`; + this.positionTooltip(pageX, pageY); + this.tooltipEl.classList.add('visible'); + } + + positionTooltip(pageX, pageY) { + if (!this.tooltipEl) return; + const offset = 12; + let left = pageX + offset; + let top = pageY + offset; + const { innerWidth, innerHeight } = window; + const rect = this.tooltipEl.getBoundingClientRect(); + if (left + rect.width > innerWidth - 8) left = pageX - rect.width - offset; + if (top + rect.height > innerHeight - 8) top = pageY - rect.height - offset; + this.tooltipEl.style.left = `${Math.max(8, left)}px`; + this.tooltipEl.style.top = `${Math.max(8, top)}px`; + } + + moveTooltip(pageX, pageY) { + if (!this.tooltipEl || !this.tooltipEl.classList.contains('visible')) return; + this.positionTooltip(pageX, pageY); + } + + hideTooltip() { + if (this.tooltipEl) this.tooltipEl.classList.remove('visible'); + } + updateDimensions(container) { // Get the container's actual dimensions const rect = container.getBoundingClientRect(); @@ -377,15 +426,25 @@ class TopologyGraphComponent extends Component { node.append('text') .text(d => d.ip) .attr('x', 15) - .attr('y', 20) + .attr('y', 22) .attr('font-size', '11px') .attr('fill', 'var(--text-secondary)'); + // App label (between IP and Status) + node.append('text') + .text(d => (d.labels && d.labels.app) ? String(d.labels.app) : '') + .attr('x', 15) + .attr('y', 38) + .attr('font-size', '11px') + .attr('fill', 'var(--text-secondary)') + .attr('font-weight', '500') + .attr('display', d => (d.labels && d.labels.app) ? null : 'none'); + // Status text node.append('text') .text(d => d.status) .attr('x', 15) - .attr('y', 35) + .attr('y', 56) .attr('font-size', '11px') .attr('fill', d => this.getNodeColor(d.status)) .attr('font-weight', '600'); @@ -448,12 +507,18 @@ class TopologyGraphComponent extends Component { d3.select(event.currentTarget).select('circle') .attr('r', d => this.getNodeRadius(d.status) + 4) .attr('stroke-width', 3); + this.showTooltip(d, event.pageX, event.pageY); }); node.on('mouseout', (event, d) => { d3.select(event.currentTarget).select('circle') .attr('r', d => this.getNodeRadius(d.status)) .attr('stroke-width', 2); + this.hideTooltip(); + }); + + node.on('mousemove', (event, d) => { + this.moveTooltip(event.pageX, event.pageY); }); link.on('mouseover', (event, d) => { @@ -696,7 +761,7 @@ class TopologyGraphComponent extends Component { hostname: nodeData.hostname, status: this.normalizeStatus(nodeData.status), latency: nodeData.latency, - labels: nodeData.resources || {} + labels: (nodeData.labels && typeof nodeData.labels === 'object') ? nodeData.labels : (nodeData.resources || {}) }; this.memberOverlayComponent.show(memberData); diff --git a/public/scripts/view-models.js b/public/scripts/view-models.js index 44904b8..f662dae 100644 --- a/public/scripts/view-models.js +++ b/public/scripts/view-models.js @@ -536,6 +536,8 @@ class TopologyViewModel extends ViewModel { ip: member.ip, status: member.status || 'UNKNOWN', latency: member.latency || 0, + // Preserve both legacy 'resources' and preferred 'labels' + labels: (member.labels && typeof member.labels === 'object') ? member.labels : (member.resources || {}), resources: member.resources || {}, x: Math.random() * 1200 + 100, // Better spacing for 1400px width y: Math.random() * 800 + 100 // Better spacing for 1000px height diff --git a/public/styles/main.css b/public/styles/main.css index 8ac58df..7398230 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -2925,6 +2925,34 @@ p { .details-drawer-backdrop { display: none; } } +/* Topology hover tooltip for labels */ +.topology-tooltip { + position: fixed; + z-index: 1001; + pointer-events: none; + background: var(--bg-primary); + border: 1px solid var(--border-primary); + box-shadow: var(--shadow-secondary); + border-radius: 10px; + padding: 0.5rem 0.6rem; + opacity: 0; + transform: translateY(4px); + transition: opacity 0.12s ease, transform 0.12s ease; +} +.topology-tooltip.visible { + opacity: 1; + transform: translateY(0); +} +.topology-tooltip .member-labels { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} +.topology-tooltip .label-chip { + font-size: 0.72rem; + padding: 0.2rem 0.5rem; +} + /* Labels section styling in node details */ .detail-section { margin-top: 20px;