fix: sort topology devices without rendering rack elevations

This commit is contained in:
2026-07-28 14:32:34 +02:00
parent 1de5ed0565
commit bb0693bef7
5 changed files with 11 additions and 102 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ Rückseite und Verkabelung.
- Vorder- und Rückseite eines Racks getrennt betrachten
- Rackübergreifende und externe Kabelverbindungen erkennen
- Geräte in der Kabeltopologie frei verschieben
- Geräte in der Kabeltopologie zunächst entsprechend ihrer Rackposition und Rackseite anordnen
- Geräte in der Kabeltopologie innerhalb jedes Racks nach ihrer Rackposition sortieren
- 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
@@ -63,42 +63,10 @@ html[data-netbox-color-mode="dark"] .rack-device {
display: flex;
flex-direction: column;
gap: .75rem;
width: 30rem;
width: 15rem;
}
.rack-topology-column.is-external { width: 15rem; }
.rack-topology-column h4 { text-align: center; font-size: .85rem; }
.rack-topology-rack-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .5rem;
height: calc(var(--rack-units) * 22px);
min-height: 280px;
margin-top: 1rem;
}
.rack-topology-lane {
position: relative;
border: 3px solid #718096;
background: repeating-linear-gradient(to top, rgba(56, 73, 89, .14) 0, rgba(56, 73, 89, .14) 1px, transparent 1px, transparent calc(100% / var(--rack-units)));
}
.rack-topology-lane-label {
position: absolute;
top: -1.4rem;
left: 0;
right: 0;
text-align: center;
font-size: .65rem;
text-transform: uppercase;
}
.rack-topology-external-body { display: flex; flex-direction: column; gap: .75rem; }
.rack-topology-lane .rack-topology-node {
position: absolute;
left: 3px;
right: 3px;
min-height: 18px;
padding: 2px 6px;
overflow: hidden;
box-sizing: border-box;
}
.rack-topology-device-list { display: flex; flex-direction: column; gap: .75rem; }
.rack-topology-node {
display: block;
padding: .55rem .75rem;
@@ -163,10 +131,3 @@ html[data-netbox-color-mode="dark"] .rack-device {
border-top: 1px solid var(--tblr-border-color, #ced4da);
}
.rack-topology-tooltip small { opacity: .65; }
html[data-bs-theme="dark"] .rack-topology-lane,
body[data-bs-theme="dark"] .rack-topology-lane,
html[data-netbox-color-mode="dark"] .rack-topology-lane {
border-color: #3b4654;
background-color: #212b36;
background-image: repeating-linear-gradient(to top, rgba(200, 215, 230, .12) 0, rgba(200, 215, 230, .12) 1px, transparent 1px, transparent calc(100% / var(--rack-units)));
}
+3 -14
View File
@@ -54,7 +54,7 @@ html[data-netbox-color-mode=dark] #visgraph {
.rack-topology { position: relative; min-height: 18rem; overflow-x: auto; padding: 1rem; background: var(--tblr-bg-surface-secondary, rgba(127,127,127,.05)); }
.rack-topology-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
.rack-topology-columns { position: relative; z-index: 2; display: flex; align-items: flex-start; justify-content: space-around; gap: 5rem; min-width: max-content; pointer-events: none; }
.rack-topology-column { display: flex; flex-direction: column; gap: .75rem; width: 30rem; }
.rack-topology-column { display: flex; flex-direction: column; gap: .75rem; width: 15rem; }
.rack-topology-column h4 { text-align: center; font-size: .85rem; }
.rack-topology-node { display: block; padding: .55rem .75rem; border: 2px solid var(--node-color); border-radius: .35rem; background: var(--tblr-bg-surface, #fff); color: inherit; text-decoration: none; box-shadow: 0 2px 6px rgba(0,0,0,.12); cursor: grab; touch-action: none; user-select: none; pointer-events: auto; }
.rack-topology-node.is-dragging { cursor: grabbing; z-index: 5; }
@@ -76,21 +76,10 @@ html[data-netbox-color-mode=dark] #visgraph {
background-color: #fff;
}
.rack-topology-rack-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .5rem;
height: calc(var(--rack-units) * 22px);
min-height: 280px;
margin-top: 1rem;
}
.rack-topology-lane { position: relative; border: 3px solid #718096; }
.rack-topology-lane .rack-topology-node { position: absolute; left: 3px; right: 3px; min-height: 18px; box-sizing: border-box; }
.rack-topology-external-body { display: flex; flex-direction: column; gap: .75rem; }
.rack-topology-device-list { display: flex; flex-direction: column; gap: .75rem; }
html[data-bs-theme="dark"], body[data-bs-theme="dark"], html[data-netbox-color-mode="dark"] {
.rack-frame, .rack-topology-lane { border-color: #3b4654; background-color: #212b36; }
.rack-frame { border-color: #3b4654; background-color: #212b36; }
.rack-device { background: #162e45; color: #fff; }
}
@@ -161,25 +161,12 @@
groups.forEach(group => {
const column = document.createElement('section');
column.className = `rack-topology-column ${group.rackUnits ? 'is-rack' : 'is-external'}`;
column.className = 'rack-topology-column';
const heading = document.createElement('h4');
heading.textContent = group.name;
column.appendChild(heading);
const nodeContainer = document.createElement('div');
nodeContainer.className = group.rackUnits ? 'rack-topology-rack-body' : 'rack-topology-external-body';
if (group.rackUnits) {
nodeContainer.style.setProperty('--rack-units', group.rackUnits);
['front', 'rear'].forEach(face => {
const lane = document.createElement('div');
lane.className = `rack-topology-lane is-${face}`;
lane.dataset.face = face;
const label = document.createElement('span');
label.className = 'rack-topology-lane-label';
label.textContent = face === 'front' ? 'Vorderseite' : 'Rückseite';
lane.appendChild(label);
nodeContainer.appendChild(lane);
});
}
nodeContainer.className = 'rack-topology-device-list';
column.appendChild(nodeContainer);
group.nodes.sort((a, b) => (b.position || 0) - (a.position || 0)).forEach(node => {
const item = document.createElement(node.url ? 'a' : 'div');
@@ -193,16 +180,9 @@
const portElement = document.createElement('small');
portElement.textContent = node.subtitle || node.name;
item.append(nameElement, portElement);
if (group.rackUnits) {
item.style.bottom = `${((node.position || 1) - 1) / group.rackUnits * 100}%`;
item.style.height = `${Math.max(node.height || 1, 1) / group.rackUnits * 100}%`;
}
const saved = storedPositions[node.node_id];
if (saved) item.style.transform = `translate(${saved.x}px, ${saved.y}px)`;
const target = group.rackUnits
? nodeContainer.querySelector(`.rack-topology-lane.is-${node.face === 'rear' ? 'rear' : 'front'}`)
: nodeContainer;
target.appendChild(item);
nodeContainer.appendChild(item);
});
columns.appendChild(column);
});
@@ -457,13 +437,6 @@
const headingY = headingBounds.top - rootBounds.top + root.scrollTop;
content += `<text x="${headingX + headingBounds.width / 2}" y="${headingY + 15}" text-anchor="middle" fill="${palette.heading}" font-family="sans-serif" font-size="13" font-weight="600">${xmlEscape(heading.textContent)}</text>`;
drawioCells += `<mxCell id="group-${drawioId++}" value="${xmlEscape(heading.textContent)}" style="text;html=1;strokeColor=none;fillColor=none;align=center;fontStyle=1;fontColor=${palette.heading};" vertex="1" parent="1"><mxGeometry x="${headingX}" y="${headingY}" width="${headingBounds.width}" height="22" as="geometry"/></mxCell>`;
column.querySelectorAll('.rack-topology-lane').forEach(lane => {
const bounds = lane.getBoundingClientRect();
const x = bounds.left - rootBounds.left + root.scrollLeft;
const y = bounds.top - rootBounds.top + root.scrollTop;
content += `<rect x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" fill="none" stroke="${palette.frame}" stroke-width="3"/>`;
drawioCells += `<mxCell id="lane-${drawioId++}" value="${xmlEscape(lane.dataset.face === 'rear' ? 'Rückseite' : 'Vorderseite')}" style="rounded=0;html=1;fillColor=none;strokeColor=${palette.frame};strokeWidth=3;verticalAlign=top;fontSize=9;fontColor=${palette.muted};" vertex="1" parent="1"><mxGeometry x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" as="geometry"/></mxCell>`;
});
});
root.querySelectorAll('.rack-topology-node').forEach(node => {
const bounds = node.getBoundingClientRect();
+2 -16
View File
@@ -992,21 +992,7 @@ class RackElevationView(PermissionRequiredMixin, View):
})
cable_rows = []
topology_nodes = {
f"device-{device.pk}": {
"node_id": f"device-{device.pk}", "device_id": device.pk,
"device_name": device.name or str(device.device_type),
"name": "", "subtitle": str(device.device_type),
"rack_id": device.rack_id, "rack_name": device.rack.name,
"url": device.get_absolute_url(),
"color": str(device.role.color or "1685fc"),
"position": float(device.position or 1),
"height": max(device.device_type.u_height or 1, 1),
"face": device.elevation_face,
"rack_u_height": device.rack.u_height,
}
for rack in rack_list for device in rack.elevation_devices
}
topology_nodes = {}
topology_edges = []
if device_ids:
cable_ids = CableTermination.objects.filter(
@@ -1022,7 +1008,7 @@ class RackElevationView(PermissionRequiredMixin, View):
cable_rows.append({"cable": cable, "a": a, "b": b})
for endpoint in (a, b):
if endpoint:
topology_nodes.setdefault(endpoint["node_id"], endpoint)
topology_nodes[endpoint["node_id"]] = endpoint
if a and b:
topology_edges.append({
"from": a["node_id"], "to": b["node_id"],