feat: improve rack readability and add cable topology

This commit is contained in:
2026-07-22 09:41:03 +02:00
parent a967f88401
commit d8069edd73
4 changed files with 190 additions and 3 deletions
@@ -6,6 +6,7 @@
{% block head %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/rack_elevation.css' %}">
{% endblock %}
{% block content %}
@@ -67,6 +68,17 @@
{% endfor %}
</div>
<div class="card mt-4">
<div class="card-header"><strong>Kabeltopologie</strong></div>
<div class="card-body">
<div id="rack-topology" class="rack-topology" aria-label="Topologie der ausgewählten Racks">
<svg class="rack-topology-lines" aria-hidden="true"></svg>
<div class="rack-topology-columns"></div>
</div>
</div>
</div>
{{ rack_topology|json_script:"rack-topology-data" }}
<div class="card mt-4">
<div class="card-header"><strong>Verkabelung der angezeigten Racks</strong> <span class="badge bg-secondary">{{ cables|length }}</span></div>
<div class="table-responsive">
@@ -91,3 +103,77 @@
<div class="alert alert-info">Wähle ein oder mehrere Racks oder grenze die Ansicht über Standort und Location ein.</div>
{% endif %}
{% endblock %}
{% block javascript %}
<script>
(() => {
const root = document.getElementById('rack-topology');
const source = document.getElementById('rack-topology-data');
if (!root || !source) return;
const data = JSON.parse(source.textContent);
const columns = root.querySelector('.rack-topology-columns');
const svg = root.querySelector('svg');
const groups = new Map();
data.nodes.forEach(node => {
const groupName = node.rack_name || 'Extern';
if (!groups.has(groupName)) groups.set(groupName, []);
groups.get(groupName).push(node);
});
groups.forEach((nodes, name) => {
const column = document.createElement('section');
column.className = 'rack-topology-column';
const heading = document.createElement('h4');
heading.textContent = name;
column.appendChild(heading);
nodes.forEach(node => {
const item = document.createElement(node.url ? 'a' : 'div');
item.className = 'rack-topology-node';
item.id = `topology-${node.node_id}`;
item.style.setProperty('--node-color', `#${node.color}`);
if (node.url) item.href = node.url;
const nameElement = document.createElement('strong');
nameElement.textContent = node.device_name;
const portElement = document.createElement('small');
portElement.textContent = node.name;
item.append(nameElement, portElement);
column.appendChild(item);
});
columns.appendChild(column);
});
const drawEdges = () => {
const bounds = root.getBoundingClientRect();
const width = Math.max(root.clientWidth, columns.scrollWidth + 32);
const height = Math.max(root.clientHeight, columns.scrollHeight + 32);
svg.style.width = `${width}px`;
svg.style.height = `${height}px`;
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
svg.replaceChildren();
data.edges.forEach(edge => {
const from = document.getElementById(`topology-${edge.from}`);
const to = document.getElementById(`topology-${edge.to}`);
if (!from || !to) return;
const a = from.getBoundingClientRect(), b = to.getBoundingClientRect();
const x1 = a.left + a.width / 2 - bounds.left;
const y1 = a.top + a.height / 2 - bounds.top;
const x2 = b.left + b.width / 2 - bounds.left;
const y2 = b.top + b.height / 2 - bounds.top;
const bend = Math.max(30, Math.abs(x2 - x1) * .35);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
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');
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
title.textContent = edge.label;
path.appendChild(title);
if (edge.url) path.addEventListener('click', () => window.location.href = edge.url);
svg.appendChild(path);
});
};
requestAnimationFrame(drawEdges);
window.addEventListener('resize', drawEdges);
})();
</script>
{% endblock %}