From 4680209d832f496aaf84a9f31705843a8337c2dc Mon Sep 17 00:00:00 2001 From: Louis Date: Tue, 28 Jul 2026 14:36:08 +0200 Subject: [PATCH] feat: add fullscreen topology and wireless links --- README.md | 2 ++ .../css/rack_elevation.css | 14 +++++++++- netbox_topology_views/static_dev/css/app.scss | 6 ++++- .../netbox_topology_views/rack_elevation.html | 27 +++++++++++++++---- netbox_topology_views/views.py | 22 +++++++++++++++ 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index beaaeea..89d8530 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Rückseite und Verkabelung. - Rackübergreifende und externe Kabelverbindungen erkennen - Geräte in der Kabeltopologie frei verschieben - Geräte in der Kabeltopologie innerhalb jedes Racks nach ihrer Rackposition sortieren +- Kabeltopologie in den Browser-Vollbildmodus schalten +- WLAN-Verbindungen als violette, gestrichelte Funkstrecken darstellen - Manuelle Positionen pro Rack-Auswahl im Browser speichern - Verbindungen eines Geräts beim Hover weiß hervorheben - Alle verbundenen Ports und Gegenstellen eines Geräts anzeigen 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 160cf20..6d88487 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: .75rem; } +.rack-topology-device-list { display: flex; flex-direction: column; gap: 14px; } .rack-topology-node { display: block; padding: .55rem .75rem; @@ -106,6 +106,18 @@ html[data-netbox-color-mode="dark"] .rack-device { filter: drop-shadow(0 0 5px #fff); } .rack-topology-edge.is-dimmed { opacity: .12; } +.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); +} +#rack-topology-card:fullscreen .card-body { min-height: 0; flex: 1; } +#rack-topology-card:fullscreen .rack-topology { height: calc(100vh - 5.5rem); } .rack-topology-tooltip { display: none; position: absolute; diff --git a/netbox_topology_views/static_dev/css/app.scss b/netbox_topology_views/static_dev/css/app.scss index b0f12f2..cc7d3c2 100644 --- a/netbox_topology_views/static_dev/css/app.scss +++ b/netbox_topology_views/static_dev/css/app.scss @@ -76,7 +76,11 @@ html[data-netbox-color-mode=dark] #visgraph { background-color: #fff; } -.rack-topology-device-list { display: flex; flex-direction: column; gap: .75rem; } +.rack-topology-device-list { display: flex; flex-direction: column; gap: 14px; } +.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); } +#rack-topology-card:fullscreen .card-body { min-height: 0; flex: 1; } +#rack-topology-card:fullscreen .rack-topology { height: calc(100vh - 5.5rem); } html[data-bs-theme="dark"], body[data-bs-theme="dark"], html[data-netbox-color-mode="dark"] { .rack-frame { border-color: #3b4654; background-color: #212b36; } 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 18f253b..3cb9e98 100644 --- a/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html +++ b/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html @@ -82,7 +82,7 @@ {% endfor %} -
+
Kabeltopologie
@@ -94,6 +94,9 @@ +
@@ -144,6 +147,8 @@ const svg = root.querySelector('svg'); const tooltip = root.querySelector('.rack-topology-tooltip'); const resetButton = document.getElementById('rack-topology-reset'); + const fullscreenButton = document.getElementById('rack-topology-fullscreen'); + const topologyCard = document.getElementById('rack-topology-card'); const storageKey = `netbox-rack-topology-v2:${window.location.pathname}${window.location.search}`; let storedPositions = {}; try { storedPositions = JSON.parse(localStorage.getItem(storageKey) || '{}'); } catch (_) {} @@ -224,6 +229,7 @@ path.setAttribute('d', `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`); path.setAttribute('stroke', edge.color); path.classList.add('rack-topology-edge'); + if (edge.kind === 'wireless') path.classList.add('is-wireless'); path.dataset.from = edge.from; path.dataset.to = edge.to; const title = document.createElementNS('http://www.w3.org/2000/svg', 'title'); @@ -233,7 +239,7 @@ path.addEventListener('mouseenter', event => { path.classList.add('is-highlighted'); showTooltip( - `${escapeHtml(edge.label)}` + + `${edge.kind === 'wireless' ? 'WLAN · ' : ''}${escapeHtml(edge.label)}` + `${escapeHtml(edge.from_device)} · ${escapeHtml(edge.from_port)}` + `↔ ${escapeHtml(edge.to_device)} · ${escapeHtml(edge.to_port)}`, event.target @@ -261,7 +267,7 @@ const localPort = outgoing ? edge.from_port : edge.to_port; const remoteDevice = outgoing ? edge.to_device : edge.from_device; const remotePort = outgoing ? edge.to_port : edge.from_port; - return `
  • ${escapeHtml(localPort)}→ ${escapeHtml(remoteDevice)} · ${escapeHtml(remotePort)}${escapeHtml(edge.label)}
  • `; + return `
  • ${escapeHtml(localPort)}→ ${escapeHtml(remoteDevice)} · ${escapeHtml(remotePort)}${edge.kind === 'wireless' ? 'WLAN · ' : ''}${escapeHtml(edge.label)}
  • `; }).join(''); showTooltip(`Verbindungen (${connections.length})
      ${rows || '
    • Keine Verbindungen
    • '}
    `, node); }); @@ -311,6 +317,17 @@ root.querySelectorAll('.rack-topology-node').forEach(node => node.style.transform = ''); drawEdges(); }); + fullscreenButton?.addEventListener('click', async () => { + if (document.fullscreenElement === topologyCard) await document.exitFullscreen(); + else await topologyCard.requestFullscreen(); + }); + document.addEventListener('fullscreenchange', () => { + const active = document.fullscreenElement === topologyCard; + fullscreenButton.innerHTML = active + ? ' Vollbild verlassen' + : ' Vollbild'; + requestAnimationFrame(drawEdges); + }); const xmlEscape = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' @@ -458,10 +475,10 @@ const a = positions.get(edge.from), b = positions.get(edge.to); if (!a || !b) return; const bend = Math.max(30, Math.abs(b.x - a.x) * .35); - edgeContent += ``; + edgeContent += ``; const sourceId = nodeCells.get(edge.from), targetId = nodeCells.get(edge.to); if (sourceId && targetId) { - drawioCells += ``; + drawioCells += ``; } }); return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height}; diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index 34c5048..ae93487 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -1017,8 +1017,30 @@ class RackElevationView(PermissionRequiredMixin, View): "label": cable.label or str(cable), "color": f"#{cable.color}" if cable.color else "#1685fc", "url": cable.get_absolute_url(), + "kind": "cable", }) + wireless_links = WirelessLink.objects.restrict(request.user, "view").filter( + Q(_interface_a_device_id__in=device_ids) | + Q(_interface_b_device_id__in=device_ids) + ).select_related( + "interface_a__device__rack", "interface_a__device__role", "interface_a__device__device_type", + "interface_b__device__rack", "interface_b__device__role", "interface_b__device__device_type", + ) + for wireless_link in wireless_links: + a = _rack_endpoint(wireless_link.interface_a) + b = _rack_endpoint(wireless_link.interface_b) + topology_nodes[a["node_id"]] = a + topology_nodes[b["node_id"]] = b + topology_edges.append({ + "from": a["node_id"], "to": b["node_id"], + "from_device": a["device_name"], "to_device": b["device_name"], + "from_port": a["name"], "to_port": b["name"], + "label": wireless_link.ssid or str(wireless_link), + "color": "#8b5cf6", "url": wireless_link.get_absolute_url(), + "kind": "wireless", + }) + return render(request, "netbox_topology_views/rack_elevation.html", { "filter_form": RackElevationFilterForm(request.GET or None), "racks": rack_list, "cables": cable_rows,