fix: detect power cables by termination type and move toggle into topology card
Power-cable detection previously relied on cable.type, which is usually unset for power patch cables (e.g. PDU-to-PDU), so they never got the distinct color. Power cables are now also detected by termination component type (PowerPort/PowerOutlet/PowerFeed). Wireless/power display is now toggled client-side via checkboxes in the Kabeltopologie card header (before the SVG/PNG/draw.io export buttons) instead of a server round-trip through the top filter form, so toggling is instant and affects exports too. Bump plugin version to 4.5.2. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,7 +127,7 @@ sudo systemctl restart netbox netbox-rq
|
||||
```
|
||||
|
||||
`--force-reinstall` ist derzeit sinnvoll, da der Fork intern noch die
|
||||
Paketversion `4.5.1` verwendet.
|
||||
Paketversion `4.5.2` verwendet.
|
||||
|
||||
## Verwendung
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
|
||||
name = "netbox_topology_views"
|
||||
verbose_name = "Topology views"
|
||||
description = "A plugin to render topology maps"
|
||||
version = "4.5.1"
|
||||
version = "4.5.2"
|
||||
author = "Mattijs Vanhaverbeke, Codex & Claude"
|
||||
author_email = "author@example.com"
|
||||
base_url = "netbox_topology_views"
|
||||
|
||||
@@ -379,12 +379,6 @@ class RackElevationFilterForm(forms.Form):
|
||||
include_empty = forms.BooleanField(
|
||||
required=False, initial=False, label=_("Include empty racks")
|
||||
)
|
||||
show_wireless = forms.BooleanField(
|
||||
required=False, initial=True, label=_("Show wireless links")
|
||||
)
|
||||
show_power = forms.BooleanField(
|
||||
required=False, initial=True, label=_("Show power cables")
|
||||
)
|
||||
|
||||
|
||||
class CoordinateGroupsForm(NetBoxModelForm):
|
||||
|
||||
@@ -85,7 +85,17 @@
|
||||
<div id="rack-topology-card" class="card mt-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>Kabeltopologie</strong>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<div class="d-flex gap-3 align-items-center rack-topology-kind-toggles">
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="rack-topology-show-wireless" checked>
|
||||
<label class="form-check-label" for="rack-topology-show-wireless">Funk/WLAN</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="rack-topology-show-power" checked>
|
||||
<label class="form-check-label" for="rack-topology-show-power">Strom</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-outline-primary topology-export" data-format="svg">SVG</button>
|
||||
<button class="btn btn-sm btn-outline-primary topology-export" data-format="png">PNG</button>
|
||||
@@ -149,6 +159,22 @@
|
||||
const data = JSON.parse(source.textContent);
|
||||
const isWirelessEdge = edge => edge.kind === 'wireless' || edge.kind === 'wireless-lan';
|
||||
const isPowerEdge = edge => edge.kind === 'power';
|
||||
const showWirelessCheckbox = document.getElementById('rack-topology-show-wireless');
|
||||
const showPowerCheckbox = document.getElementById('rack-topology-show-power');
|
||||
const isEdgeVisible = edge => {
|
||||
if (isWirelessEdge(edge)) return showWirelessCheckbox ? showWirelessCheckbox.checked : true;
|
||||
if (isPowerEdge(edge)) return showPowerCheckbox ? showPowerCheckbox.checked : true;
|
||||
return true;
|
||||
};
|
||||
const applyKindVisibility = () => {
|
||||
const showWireless = showWirelessCheckbox ? showWirelessCheckbox.checked : true;
|
||||
root.querySelectorAll('.rack-topology-node').forEach(node => {
|
||||
if (node.dataset.kind === 'wireless-lan') {
|
||||
node.style.display = showWireless ? '' : 'none';
|
||||
}
|
||||
});
|
||||
drawEdges();
|
||||
};
|
||||
const rackData = JSON.parse(document.getElementById('rack-export-data').textContent);
|
||||
const columns = root.querySelector('.rack-topology-columns');
|
||||
const svg = root.querySelector('svg');
|
||||
@@ -224,6 +250,7 @@
|
||||
item.id = `topology-${node.node_id}`;
|
||||
item.style.setProperty('--node-color', `#${node.color}`);
|
||||
item.dataset.nodeId = node.node_id;
|
||||
if (node.kind) item.dataset.kind = node.kind;
|
||||
if (node.url) item.href = node.url;
|
||||
const nameElement = document.createElement('strong');
|
||||
nameElement.textContent = node.device_name;
|
||||
@@ -271,6 +298,7 @@
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
svg.replaceChildren();
|
||||
data.edges.forEach(edge => {
|
||||
if (!isEdgeVisible(edge)) return;
|
||||
const from = document.getElementById(`topology-${edge.from}`);
|
||||
const to = document.getElementById(`topology-${edge.to}`);
|
||||
if (!from || !to) return;
|
||||
@@ -309,7 +337,7 @@
|
||||
});
|
||||
};
|
||||
|
||||
const connectedEdges = nodeId => data.edges.filter(edge => edge.from === nodeId || edge.to === nodeId);
|
||||
const connectedEdges = nodeId => data.edges.filter(edge => isEdgeVisible(edge) && (edge.from === nodeId || edge.to === nodeId));
|
||||
root.querySelectorAll('.rack-topology-node').forEach(node => {
|
||||
const nodeId = node.dataset.nodeId;
|
||||
node.addEventListener('mouseenter', () => {
|
||||
@@ -538,6 +566,7 @@
|
||||
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>`;
|
||||
});
|
||||
root.querySelectorAll('.rack-topology-node').forEach(node => {
|
||||
if (node.style.display === 'none') return;
|
||||
const bounds = node.getBoundingClientRect();
|
||||
const x = bounds.left - rootBounds.left + root.scrollLeft;
|
||||
const y = bounds.top - rootBounds.top + root.scrollTop;
|
||||
@@ -554,6 +583,7 @@
|
||||
});
|
||||
let edgeContent = '';
|
||||
data.edges.forEach(edge => {
|
||||
if (!isEdgeVisible(edge)) return;
|
||||
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);
|
||||
@@ -613,7 +643,9 @@
|
||||
document.querySelectorAll('.topology-export').forEach(button => button.addEventListener('click', () => {
|
||||
exportGraphic(buildTopologySvg(), button.dataset.format, 'rack-topologie');
|
||||
}));
|
||||
requestAnimationFrame(drawEdges);
|
||||
showWirelessCheckbox?.addEventListener('change', applyKindVisibility);
|
||||
showPowerCheckbox?.addEventListener('change', applyKindVisibility);
|
||||
requestAnimationFrame(applyKindVisibility);
|
||||
window.addEventListener('resize', drawEdges);
|
||||
root.addEventListener('scroll', drawEdges, {passive: true});
|
||||
})();
|
||||
|
||||
@@ -1009,13 +1009,7 @@ class RackElevationView(PermissionRequiredMixin, View):
|
||||
} for device in rack.elevation_devices],
|
||||
})
|
||||
|
||||
def _flag_enabled(name):
|
||||
if not request.GET:
|
||||
return True
|
||||
return request.GET.get(name) in ("on", "True", "true", "1")
|
||||
|
||||
show_wireless = _flag_enabled("show_wireless")
|
||||
show_power = _flag_enabled("show_power")
|
||||
power_component_models = {"powerport", "poweroutlet", "powerfeed"}
|
||||
|
||||
cable_rows = []
|
||||
topology_nodes = {}
|
||||
@@ -1026,10 +1020,10 @@ class RackElevationView(PermissionRequiredMixin, View):
|
||||
).values_list("cable_id", flat=True).distinct()
|
||||
cables = Cable.objects.restrict(request.user, "view").filter(pk__in=cable_ids)
|
||||
for cable in cables.order_by("label", "pk"):
|
||||
is_power_cable = cable.type == CableTypeChoices.TYPE_POWER
|
||||
if is_power_cable and not show_power:
|
||||
continue
|
||||
a_terms, b_terms = list(cable.a_terminations), list(cable.b_terminations)
|
||||
is_power_cable = cable.type == CableTypeChoices.TYPE_POWER or any(
|
||||
term._meta.model_name in power_component_models for term in a_terms + b_terms
|
||||
)
|
||||
for index in range(max(len(a_terms), len(b_terms), 1)):
|
||||
a = _rack_endpoint(a_terms[index]) if index < len(a_terms) else None
|
||||
b = _rack_endpoint(b_terms[index]) if index < len(b_terms) else None
|
||||
@@ -1050,7 +1044,6 @@ class RackElevationView(PermissionRequiredMixin, View):
|
||||
"kind": "power" if is_power_cable else "cable",
|
||||
})
|
||||
|
||||
if show_wireless:
|
||||
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)
|
||||
@@ -1084,6 +1077,7 @@ class RackElevationView(PermissionRequiredMixin, View):
|
||||
"rack_id": None, "rack_name": "Funknetze",
|
||||
"url": wireless_lan.get_absolute_url(), "color": "8b5cf6",
|
||||
"position": None, "height": 1, "face": "front", "rack_u_height": None,
|
||||
"kind": "wireless-lan",
|
||||
}
|
||||
wlan_interfaces = Interface.objects.restrict(request.user, "view").filter(
|
||||
wireless_lans=wireless_lan
|
||||
|
||||
Reference in New Issue
Block a user