From ef1ad4d60785b9bf21068efcf9ce66a94d31908a Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 28 Jul 2026 14:58:13 +0200 Subject: [PATCH] fix: enforce topology spacing and light-mode cable highlights --- .../css/rack_elevation.css | 6 +-- netbox_topology_views/static_dev/css/app.scss | 4 +- .../netbox_topology_views/rack_elevation.html | 52 ++++++++++++++----- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/netbox_topology_views/static/netbox_topology_views/css/rack_elevation.css b/netbox_topology_views/static/netbox_topology_views/css/rack_elevation.css index a53b249..59c0715 100644 --- a/netbox_topology_views/static/netbox_topology_views/css/rack_elevation.css +++ b/netbox_topology_views/static/netbox_topology_views/css/rack_elevation.css @@ -66,7 +66,7 @@ html[data-netbox-color-mode="dark"] .rack-device { width: 15rem; } .rack-topology-column h4 { text-align: center; font-size: .85rem; } -.rack-topology-device-list { display: flex; flex-direction: column; gap: 20px; } +.rack-topology-device-list { display: flex; flex-direction: column; gap: 40px; } .rack-topology-node { display: block; padding: .55rem .75rem; @@ -100,10 +100,10 @@ html[data-netbox-color-mode="dark"] .rack-device { transition: opacity .12s, stroke .12s, filter .12s, stroke-width .12s; } .rack-topology-edge.is-highlighted { - stroke: var(--topology-highlight-color, #22c55e) !important; + stroke: var(--topology-highlight-color, #16a34a) !important; stroke-width: 5; opacity: 1; - filter: drop-shadow(0 0 5px var(--topology-highlight-color, #22c55e)); + filter: drop-shadow(0 0 5px var(--topology-highlight-color, #16a34a)); } .rack-topology-edge.is-dimmed { opacity: .12; } .rack-topology-edge.is-wireless { diff --git a/netbox_topology_views/static_dev/css/app.scss b/netbox_topology_views/static_dev/css/app.scss index 8deeab1..cf1925a 100644 --- a/netbox_topology_views/static_dev/css/app.scss +++ b/netbox_topology_views/static_dev/css/app.scss @@ -62,7 +62,7 @@ html[data-netbox-color-mode=dark] #visgraph { .rack-topology-node strong, .rack-topology-node small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rack-topology-node small { opacity: .7; } .rack-topology-edge { fill: none; stroke-width: 3; opacity: .8; pointer-events: stroke; cursor: pointer; transition: opacity .12s, stroke .12s, filter .12s, stroke-width .12s; } -.rack-topology-edge.is-highlighted { stroke: var(--topology-highlight-color, #22c55e) !important; stroke-width: 5; opacity: 1; filter: drop-shadow(0 0 5px var(--topology-highlight-color, #22c55e)); } +.rack-topology-edge.is-highlighted { stroke: var(--topology-highlight-color, #16a34a) !important; stroke-width: 5; opacity: 1; filter: drop-shadow(0 0 5px var(--topology-highlight-color, #16a34a)); } .rack-topology-edge.is-dimmed { opacity: .12; } .rack-topology-tooltip { display: none; position: absolute; z-index: 10; width: max-content; max-width: 28rem; padding: .65rem .8rem; border: 1px solid var(--tblr-border-color, #ced4da); border-radius: .35rem; background: var(--tblr-bg-surface, #fff); box-shadow: 0 4px 18px rgba(0,0,0,.25); pointer-events: none; } .rack-topology-tooltip.is-visible { display: block; } @@ -77,7 +77,7 @@ html[data-netbox-color-mode=dark] #visgraph { background-color: #fff; } -.rack-topology-device-list { display: flex; flex-direction: column; gap: 20px; } +.rack-topology-device-list { display: flex; flex-direction: column; gap: 40px; } .rack-topology-edge.is-wireless { stroke-dasharray: 7 7; stroke-linecap: round; } #rack-topology-card:fullscreen { margin: 0 !important; border: 0; border-radius: 0; background: var(--tblr-bg-surface, #fff); display: flex; flex-direction: column; overflow: hidden; } #rack-topology-card:fullscreen .card-body { min-height: 0; flex: 1; overflow: hidden; } diff --git a/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html b/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html index 11886d7..9329446 100644 --- a/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html +++ b/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html @@ -160,23 +160,32 @@ const zoomResetButton = document.getElementById('rack-topology-zoom-reset'); const zoomLabel = document.getElementById('rack-topology-zoom-label'); let topologyZoom = 1; - const activeNetBoxTheme = () => document.body.dataset.bsTheme || - document.documentElement.dataset.bsTheme || - document.documentElement.dataset.netboxColorMode || 'light'; + const activeNetBoxTheme = () => { + for (const element of [topologyCard, document.body, document.documentElement]) { + const match = getComputedStyle(element).backgroundColor.match(/[\d.]+/g); + if (match && match.length >= 3 && (match.length < 4 || Number(match[3]) > 0)) { + const [red, green, blue] = match.map(Number); + return (.2126 * red + .7152 * green + .0722 * blue) < 140 ? 'dark' : 'light'; + } + } + return document.body.dataset.bsTheme || document.documentElement.dataset.bsTheme || 'light'; + }; + const topologyHighlightColor = () => activeNetBoxTheme() === 'dark' ? '#ffffff' : '#16a34a'; const applyTopologyTheme = () => { - root.style.setProperty( - '--topology-highlight-color', - activeNetBoxTheme() === 'dark' ? '#ffffff' : '#22c55e' - ); + root.style.setProperty('--topology-highlight-color', topologyHighlightColor()); + root.querySelectorAll('.rack-topology-edge.is-highlighted').forEach(path => { + path.style.setProperty('stroke', topologyHighlightColor(), 'important'); + path.style.setProperty('filter', `drop-shadow(0 0 5px ${topologyHighlightColor()})`, 'important'); + }); }; applyTopologyTheme(); - const topologyThemeObserver = new MutationObserver(applyTopologyTheme); + const topologyThemeObserver = new MutationObserver(() => requestAnimationFrame(applyTopologyTheme)); topologyThemeObserver.observe(document.body, {attributes: true, attributeFilter: ['data-bs-theme']}); topologyThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme', 'data-netbox-color-mode'], }); - const storageKey = `netbox-rack-topology-v2:${window.location.pathname}${window.location.search}`; + const storageKey = `netbox-rack-topology-v3:${window.location.pathname}${window.location.search}`; let storedPositions = {}; try { storedPositions = JSON.parse(localStorage.getItem(storageKey) || '{}'); } catch (_) {} const groups = new Map(); @@ -199,6 +208,7 @@ column.appendChild(heading); const nodeContainer = document.createElement('div'); nodeContainer.className = 'rack-topology-device-list'; + nodeContainer.style.gap = '40px'; column.appendChild(nodeContainer); group.nodes.sort((a, b) => (b.position || 0) - (a.position || 0)).forEach(node => { const item = document.createElement(node.url ? 'a' : 'div'); @@ -233,6 +243,16 @@ element.textContent = value == null ? '—' : String(value); return element.innerHTML; }; + const setEdgeHighlighted = (path, highlighted) => { + path.classList.toggle('is-highlighted', highlighted); + if (highlighted) { + path.style.setProperty('stroke', topologyHighlightColor(), 'important'); + path.style.setProperty('filter', `drop-shadow(0 0 5px ${topologyHighlightColor()})`, 'important'); + } else { + path.style.removeProperty('stroke'); + path.style.removeProperty('filter'); + } + }; const drawEdges = () => { const bounds = root.getBoundingClientRect(); @@ -264,7 +284,7 @@ path.appendChild(title); if (edge.url) path.addEventListener('click', () => window.location.href = edge.url); path.addEventListener('mouseenter', event => { - path.classList.add('is-highlighted'); + setEdgeHighlighted(path, true); showTooltip( `${isWirelessEdge(edge) ? 'WLAN · ' : ''}${escapeHtml(edge.label)}` + `${escapeHtml(edge.from_device)} · ${escapeHtml(edge.from_port)}` + @@ -273,7 +293,7 @@ ); }); path.addEventListener('mouseleave', () => { - path.classList.remove('is-highlighted'); + setEdgeHighlighted(path, false); hideTooltip(); }); svg.appendChild(path); @@ -286,8 +306,9 @@ node.addEventListener('mouseenter', () => { const connections = connectedEdges(nodeId); svg.querySelectorAll('.rack-topology-edge').forEach(path => { - path.classList.toggle('is-highlighted', path.dataset.from === nodeId || path.dataset.to === nodeId); - path.classList.toggle('is-dimmed', path.dataset.from !== nodeId && path.dataset.to !== nodeId); + const connected = path.dataset.from === nodeId || path.dataset.to === nodeId; + setEdgeHighlighted(path, connected); + path.classList.toggle('is-dimmed', !connected); }); const rows = connections.map(edge => { const outgoing = edge.from === nodeId; @@ -299,7 +320,10 @@ showTooltip(`Verbindungen (${connections.length})`, node); }); node.addEventListener('mouseleave', () => { - svg.querySelectorAll('.rack-topology-edge').forEach(path => path.classList.remove('is-highlighted', 'is-dimmed')); + svg.querySelectorAll('.rack-topology-edge').forEach(path => { + setEdgeHighlighted(path, false); + path.classList.remove('is-dimmed'); + }); hideTooltip(); });