feat: declare NetBox 4.7.x support and add wireless/power toggle to Kabeltopologie
Adds min_version/max_version to the plugin config for NetBox 4.7.x and extends the rack view's cable topology with optional wireless and power cable display, rendered with distinct colors and dash patterns from regular network cables. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ Rückseite und Verkabelung.
|
|||||||
- Kabeltopologie zwischen 50 und 200 Prozent zoomen und im Vollbild scrollen
|
- Kabeltopologie zwischen 50 und 200 Prozent zoomen und im Vollbild scrollen
|
||||||
- Geräte in der Kabeltopologie mit rund 5 mm Abstand darstellen
|
- Geräte in der Kabeltopologie mit rund 5 mm Abstand darstellen
|
||||||
- P2P-WirelessLinks und Multi-Access-WLANs als violette, gestrichelte Funkstrecken darstellen
|
- P2P-WirelessLinks und Multi-Access-WLANs als violette, gestrichelte Funkstrecken darstellen
|
||||||
|
- Funk-/WLAN-Verbindungen und Stromkabel in der Kabeltopologie der Rack-Ansicht optional ein-/ausblenden, farblich abgesetzt von Netzwerkkabeln (violett gestrichelt bzw. orange gepunktet)
|
||||||
- Manuelle Positionen pro Rack-Auswahl im Browser speichern
|
- Manuelle Positionen pro Rack-Auswahl im Browser speichern
|
||||||
- Verbindungen eines Geräts beim Hover weiß hervorheben
|
- Verbindungen eines Geräts beim Hover weiß hervorheben
|
||||||
- Alle verbundenen Ports und Gegenstellen eines Geräts anzeigen
|
- Alle verbundenen Ports und Gegenstellen eines Geräts anzeigen
|
||||||
@@ -209,10 +210,11 @@ entsprechenden Rechte auf `netbox_topology_views.coordinate` benötigt.
|
|||||||
## Kompatibilität
|
## Kompatibilität
|
||||||
|
|
||||||
Die Basis dieses Forks ist NetBox Topology Views `4.5.1`. Die neuen
|
Die Basis dieses Forks ist NetBox Topology Views `4.5.1`. Die neuen
|
||||||
Rack-Funktionen wurden für den Einsatz mit NetBox `4.6.5` angepasst. Da das
|
Rack-Funktionen wurden für den Einsatz mit NetBox `4.6.5` angepasst und das
|
||||||
ursprüngliche Release offiziell für NetBox 4.5.x ausgewiesen ist, sollten
|
Plugin ist für NetBox `4.7.x` (`min_version`/`max_version` in
|
||||||
Updates auf neuere NetBox-Versionen zunächst in einer Testumgebung geprüft
|
`netbox_topology_views/__init__.py`) freigegeben. Da das ursprüngliche
|
||||||
werden.
|
Release offiziell für NetBox 4.5.x ausgewiesen ist, sollten Updates auf
|
||||||
|
neuere NetBox-Versionen zunächst in einer Testumgebung geprüft werden.
|
||||||
|
|
||||||
## Ursprung und Danksagung
|
## Ursprung und Danksagung
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ class TopologyViewsConfig(PluginConfig):
|
|||||||
verbose_name = "Topology views"
|
verbose_name = "Topology views"
|
||||||
description = "A plugin to render topology maps"
|
description = "A plugin to render topology maps"
|
||||||
version = "4.5.1"
|
version = "4.5.1"
|
||||||
author = "Mattijs Vanhaverbeke & LKE"
|
author = "Mattijs Vanhaverbeke, Codex & Claude"
|
||||||
author_email = "author@example.com"
|
author_email = "author@example.com"
|
||||||
base_url = "netbox_topology_views"
|
base_url = "netbox_topology_views"
|
||||||
|
min_version = "4.7.0"
|
||||||
|
max_version = "4.7.99"
|
||||||
required_settings = []
|
required_settings = []
|
||||||
default_settings = {
|
default_settings = {
|
||||||
"static_image_directory": "netbox_topology_views/img",
|
"static_image_directory": "netbox_topology_views/img",
|
||||||
|
|||||||
@@ -379,6 +379,12 @@ class RackElevationFilterForm(forms.Form):
|
|||||||
include_empty = forms.BooleanField(
|
include_empty = forms.BooleanField(
|
||||||
required=False, initial=False, label=_("Include empty racks")
|
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):
|
class CoordinateGroupsForm(NetBoxModelForm):
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ html[data-netbox-color-mode="dark"] .rack-device {
|
|||||||
stroke-dasharray: 7 7;
|
stroke-dasharray: 7 7;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
}
|
}
|
||||||
|
.rack-topology-edge.is-power {
|
||||||
|
stroke-dasharray: 2 4;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
#rack-topology-card:fullscreen {
|
#rack-topology-card:fullscreen {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
@@ -148,6 +148,7 @@
|
|||||||
if (!root || !source) return;
|
if (!root || !source) return;
|
||||||
const data = JSON.parse(source.textContent);
|
const data = JSON.parse(source.textContent);
|
||||||
const isWirelessEdge = edge => edge.kind === 'wireless' || edge.kind === 'wireless-lan';
|
const isWirelessEdge = edge => edge.kind === 'wireless' || edge.kind === 'wireless-lan';
|
||||||
|
const isPowerEdge = edge => edge.kind === 'power';
|
||||||
const rackData = JSON.parse(document.getElementById('rack-export-data').textContent);
|
const rackData = JSON.parse(document.getElementById('rack-export-data').textContent);
|
||||||
const columns = root.querySelector('.rack-topology-columns');
|
const columns = root.querySelector('.rack-topology-columns');
|
||||||
const svg = root.querySelector('svg');
|
const svg = root.querySelector('svg');
|
||||||
@@ -284,6 +285,7 @@
|
|||||||
path.setAttribute('stroke', edge.color);
|
path.setAttribute('stroke', edge.color);
|
||||||
path.classList.add('rack-topology-edge');
|
path.classList.add('rack-topology-edge');
|
||||||
if (isWirelessEdge(edge)) path.classList.add('is-wireless');
|
if (isWirelessEdge(edge)) path.classList.add('is-wireless');
|
||||||
|
if (isPowerEdge(edge)) path.classList.add('is-power');
|
||||||
path.dataset.from = edge.from;
|
path.dataset.from = edge.from;
|
||||||
path.dataset.to = edge.to;
|
path.dataset.to = edge.to;
|
||||||
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||||||
@@ -293,7 +295,7 @@
|
|||||||
path.addEventListener('mouseenter', event => {
|
path.addEventListener('mouseenter', event => {
|
||||||
setEdgeHighlighted(path, true);
|
setEdgeHighlighted(path, true);
|
||||||
showTooltip(
|
showTooltip(
|
||||||
`<strong>${isWirelessEdge(edge) ? 'WLAN · ' : ''}${escapeHtml(edge.label)}</strong>` +
|
`<strong>${isWirelessEdge(edge) ? 'WLAN · ' : isPowerEdge(edge) ? 'Strom · ' : ''}${escapeHtml(edge.label)}</strong>` +
|
||||||
`<span>${escapeHtml(edge.from_device)} · ${escapeHtml(edge.from_port)}</span>` +
|
`<span>${escapeHtml(edge.from_device)} · ${escapeHtml(edge.from_port)}</span>` +
|
||||||
`<span>↔ ${escapeHtml(edge.to_device)} · ${escapeHtml(edge.to_port)}</span>`,
|
`<span>↔ ${escapeHtml(edge.to_device)} · ${escapeHtml(edge.to_port)}</span>`,
|
||||||
event.target
|
event.target
|
||||||
@@ -322,7 +324,7 @@
|
|||||||
const localPort = outgoing ? edge.from_port : edge.to_port;
|
const localPort = outgoing ? edge.from_port : edge.to_port;
|
||||||
const remoteDevice = outgoing ? edge.to_device : edge.from_device;
|
const remoteDevice = outgoing ? edge.to_device : edge.from_device;
|
||||||
const remotePort = outgoing ? edge.to_port : edge.from_port;
|
const remotePort = outgoing ? edge.to_port : edge.from_port;
|
||||||
return `<li><strong>${escapeHtml(localPort)}</strong><span>→ ${escapeHtml(remoteDevice)} · ${escapeHtml(remotePort)}</span><small>${isWirelessEdge(edge) ? 'WLAN · ' : ''}${escapeHtml(edge.label)}</small></li>`;
|
return `<li><strong>${escapeHtml(localPort)}</strong><span>→ ${escapeHtml(remoteDevice)} · ${escapeHtml(remotePort)}</span><small>${isWirelessEdge(edge) ? 'WLAN · ' : isPowerEdge(edge) ? 'Strom · ' : ''}${escapeHtml(edge.label)}</small></li>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
showTooltip(`<strong>Verbindungen (${connections.length})</strong><ul>${rows || '<li>Keine Verbindungen</li>'}</ul>`, node);
|
showTooltip(`<strong>Verbindungen (${connections.length})</strong><ul>${rows || '<li>Keine Verbindungen</li>'}</ul>`, node);
|
||||||
});
|
});
|
||||||
@@ -555,10 +557,12 @@
|
|||||||
const a = positions.get(edge.from), b = positions.get(edge.to);
|
const a = positions.get(edge.from), b = positions.get(edge.to);
|
||||||
if (!a || !b) return;
|
if (!a || !b) return;
|
||||||
const bend = Math.max(30, Math.abs(b.x - a.x) * .35);
|
const bend = Math.max(30, Math.abs(b.x - a.x) * .35);
|
||||||
edgeContent += `<path d="M ${a.x} ${a.y} C ${a.x + bend} ${a.y}, ${b.x - bend} ${b.y}, ${b.x} ${b.y}" fill="none" stroke="${xmlEscape(edge.color)}" stroke-width="3"${isWirelessEdge(edge) ? ' stroke-dasharray="7 7"' : ''}/>`;
|
const dashPattern = isWirelessEdge(edge) ? '7 7' : isPowerEdge(edge) ? '2 4' : null;
|
||||||
|
const edgeLabelPrefix = isWirelessEdge(edge) ? 'WLAN · ' : isPowerEdge(edge) ? 'Strom · ' : '';
|
||||||
|
edgeContent += `<path d="M ${a.x} ${a.y} C ${a.x + bend} ${a.y}, ${b.x - bend} ${b.y}, ${b.x} ${b.y}" fill="none" stroke="${xmlEscape(edge.color)}" stroke-width="3"${dashPattern ? ` stroke-dasharray="${dashPattern}"` : ''}/>`;
|
||||||
const sourceId = nodeCells.get(edge.from), targetId = nodeCells.get(edge.to);
|
const sourceId = nodeCells.get(edge.from), targetId = nodeCells.get(edge.to);
|
||||||
if (sourceId && targetId) {
|
if (sourceId && targetId) {
|
||||||
drawioCells += `<mxCell id="edge-${drawioId++}" value="${xmlEscape(isWirelessEdge(edge) ? `WLAN · ${edge.label}` : edge.label)}" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeColor=${xmlEscape(edge.color)};strokeWidth=2;fontSize=9;${isWirelessEdge(edge) ? 'dashed=1;dashPattern=7 7;' : ''}" edge="1" parent="1" source="${sourceId}" target="${targetId}"><mxGeometry relative="1" as="geometry"/></mxCell>`;
|
drawioCells += `<mxCell id="edge-${drawioId++}" value="${xmlEscape(`${edgeLabelPrefix}${edge.label}`)}" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;endFill=0;strokeColor=${xmlEscape(edge.color)};strokeWidth=2;fontSize=9;${dashPattern ? `dashed=1;dashPattern=${dashPattern};` : ''}" edge="1" parent="1" source="${sourceId}" target="${targetId}"><mxGeometry relative="1" as="geometry"/></mxCell>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height};
|
return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from typing import DefaultDict, Dict, Optional, Union
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
|
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
|
||||||
|
from dcim.choices import CableTypeChoices
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Cable,
|
Cable,
|
||||||
CableTermination,
|
CableTermination,
|
||||||
@@ -1008,6 +1009,14 @@ class RackElevationView(PermissionRequiredMixin, View):
|
|||||||
} for device in rack.elevation_devices],
|
} 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")
|
||||||
|
|
||||||
cable_rows = []
|
cable_rows = []
|
||||||
topology_nodes = {}
|
topology_nodes = {}
|
||||||
topology_edges = []
|
topology_edges = []
|
||||||
@@ -1017,6 +1026,9 @@ class RackElevationView(PermissionRequiredMixin, View):
|
|||||||
).values_list("cable_id", flat=True).distinct()
|
).values_list("cable_id", flat=True).distinct()
|
||||||
cables = Cable.objects.restrict(request.user, "view").filter(pk__in=cable_ids)
|
cables = Cable.objects.restrict(request.user, "view").filter(pk__in=cable_ids)
|
||||||
for cable in cables.order_by("label", "pk"):
|
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)
|
a_terms, b_terms = list(cable.a_terminations), list(cable.b_terminations)
|
||||||
for index in range(max(len(a_terms), len(b_terms), 1)):
|
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
|
a = _rack_endpoint(a_terms[index]) if index < len(a_terms) else None
|
||||||
@@ -1027,16 +1039,18 @@ class RackElevationView(PermissionRequiredMixin, View):
|
|||||||
if endpoint:
|
if endpoint:
|
||||||
topology_nodes[endpoint["node_id"]] = endpoint
|
topology_nodes[endpoint["node_id"]] = endpoint
|
||||||
if a and b:
|
if a and b:
|
||||||
|
default_color = "#f59e0b" if is_power_cable else "#1685fc"
|
||||||
topology_edges.append({
|
topology_edges.append({
|
||||||
"from": a["node_id"], "to": b["node_id"],
|
"from": a["node_id"], "to": b["node_id"],
|
||||||
"from_device": a["device_name"], "to_device": b["device_name"],
|
"from_device": a["device_name"], "to_device": b["device_name"],
|
||||||
"from_port": a["name"], "to_port": b["name"],
|
"from_port": a["name"], "to_port": b["name"],
|
||||||
"label": cable.label or str(cable),
|
"label": cable.label or str(cable),
|
||||||
"color": f"#{cable.color}" if cable.color else "#1685fc",
|
"color": f"#{cable.color}" if cable.color else default_color,
|
||||||
"url": cable.get_absolute_url(),
|
"url": cable.get_absolute_url(),
|
||||||
"kind": "cable",
|
"kind": "power" if is_power_cable else "cable",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if show_wireless:
|
||||||
wireless_links = WirelessLink.objects.restrict(request.user, "view").filter(
|
wireless_links = WirelessLink.objects.restrict(request.user, "view").filter(
|
||||||
Q(interface_a__device_id__in=device_ids) |
|
Q(interface_a__device_id__in=device_ids) |
|
||||||
Q(interface_b__device_id__in=device_ids)
|
Q(interface_b__device_id__in=device_ids)
|
||||||
|
|||||||
Reference in New Issue
Block a user