From 944652f66c197a6dc6f451e0ea5454cb742512ee Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 22 Jul 2026 13:31:05 +0200 Subject: [PATCH] feat: export editable draw.io rack and topology elements --- README.md | 4 +- .../netbox_topology_views/rack_elevation.html | 51 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d0502a7..21cbacd 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,9 @@ Ein Klick öffnet das jeweilige NetBox-Objekt. Über die Export-Schaltflächen können ein einzelnes Rack, alle aktuell angezeigten Racks oder die interaktive Kabeltopologie als SVG, PNG oder draw.io-Datei gespeichert werden. Sämtliche Exportformate werden ohne -Hintergrundfläche erzeugt. +Hintergrundfläche erzeugt. Im draw.io-Export bleiben Rack-Rahmen, Geräte und +Beschriftungen als einzelne Elemente bearbeitbar. Topologiekabel werden als +echte Kanten exportiert und folgen den Geräten beim Verschieben in draw.io. Die Positionen werden im `localStorage` des Browsers gespeichert. Sie gelten damit nur für den jeweiligen Browser und die konkrete Rack-/Filterauswahl. 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 3be3f00..1592b97 100644 --- a/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html +++ b/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html @@ -308,6 +308,41 @@ const safeName = value => String(value || 'export').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-|-$/g, ''); const svgDocument = (width, height, content) => `${content}`; + const drawioDocument = cells => + `${cells}`; + + const buildRackDrawio = racks => { + const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55; + const rackWidth = labelWidth + faceWidth * 2 + faceGap; + let cells = '', cellId = 2; + const vertex = (value, style, x, y, width, height) => { + const id = cellId++; + cells += ``; + return id; + }; + racks.forEach((rack, rackIndex) => { + const baseX = 10 + rackIndex * (rackWidth + rackGap); + const rackHeight = rack.u_height * unit; + vertex(rack.name, 'text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=16;fontStyle=1;fontColor=#1685fc;', baseX, 0, rackWidth, 25); + vertex(`${rack.site}${rack.location ? ` · ${rack.location}` : ''} · ${rack.u_height}U`, 'text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontColor=#778899;', baseX, 24, rackWidth, 20); + ['front', 'rear'].forEach((face, faceIndex) => { + const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap); + vertex(face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE', 'text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=10;fontColor=#778899;', x, 38, faceWidth, 17); + vertex('', 'rounded=0;html=1;fillColor=none;strokeColor=#52606d;strokeWidth=4;', x, header, faceWidth, rackHeight); + for (let u = 1; u <= rack.u_height; u++) { + const y = header + rackHeight - u * unit; + if (faceIndex === 0) vertex(String(u), 'text;html=1;strokeColor=none;fillColor=none;align=right;fontSize=8;fontColor=#778899;', x - labelWidth, y, labelWidth - 4, unit); + } + rack.devices.filter(device => device.face === face).forEach(device => { + const y = header + rackHeight - (device.position - 1 + device.height) * unit; + const h = Math.max(device.height * unit, 4); + const value = h >= 31 ? `${device.name}
U${device.position} · ${device.type}` : device.name; + vertex(value, `rounded=1;html=1;whiteSpace=wrap;overflow=hidden;align=left;verticalAlign=top;spacingLeft=5;spacingTop=2;fillColor=#162e45;strokeColor=${device.color};strokeWidth=2;fontColor=#ffffff;fontSize=11;`, x + 3, y + 1, faceWidth - 6, h - 2); + }); + }); + }); + return drawioDocument(cells); + }; const buildRackSvg = racks => { const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55; @@ -339,7 +374,7 @@ }); }); }); - return {svg: svgDocument(width, height, content), width, height}; + return {svg: svgDocument(width, height, content), drawio: buildRackDrawio(racks), width, height}; }; const buildTopologySvg = () => { @@ -347,7 +382,9 @@ let width = Math.max(root.clientWidth, columns.scrollWidth + 32); let height = Math.max(root.clientHeight, columns.scrollHeight + 32); const positions = new Map(); + const nodeCells = new Map(); let content = ''; + let drawioCells = '', drawioId = 2; root.querySelectorAll('.rack-topology-node').forEach(node => { const bounds = node.getBoundingClientRect(); const x = bounds.left - rootBounds.left + root.scrollLeft; @@ -356,6 +393,9 @@ height = Math.max(height, y + bounds.height + 20); positions.set(node.dataset.nodeId, {x: x + bounds.width / 2, y: y + bounds.height / 2}); const color = getComputedStyle(node).getPropertyValue('--node-color').trim() || '#1685fc'; + const cellId = `node-${drawioId++}`; + nodeCells.set(node.dataset.nodeId, cellId); + drawioCells += ``; content += ``; content += `${xmlEscape(node.querySelector('strong').textContent)}`; content += `${xmlEscape(node.querySelector('small').textContent)}`; @@ -366,8 +406,12 @@ if (!a || !b) return; const bend = Math.max(30, Math.abs(b.x - a.x) * .35); edgeContent += ``; + const sourceId = nodeCells.get(edge.from), targetId = nodeCells.get(edge.to); + if (sourceId && targetId) { + drawioCells += ``; + } }); - return {svg: svgDocument(width, height, edgeContent + content), width, height}; + return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height}; }; const downloadBlob = (blob, filename) => { @@ -377,7 +421,8 @@ link.click(); setTimeout(() => URL.revokeObjectURL(link.href), 1000); }; - const asDrawio = ({svg, width, height}) => { + const asDrawio = ({svg, drawio, width, height}) => { + if (drawio) return drawio; const image = `data:image/svg+xml,${encodeURIComponent(svg)}`; return ``; };