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;