Compare commits
12
Commits
1ff68d7026
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86a8daf45e | ||
|
|
f706db754c | ||
|
|
c49270fab0 | ||
|
|
6ff2b60bb4 | ||
|
|
ef1ad4d607 | ||
|
|
a1d28e922d | ||
|
|
d9d2e4dd59 | ||
|
|
cff9282a60 | ||
|
|
1f80ae5099 | ||
|
|
4680209d83 | ||
|
|
bb0693bef7 | ||
|
|
1de5ed0565 |
@@ -13,8 +13,14 @@ Rückseite und Verkabelung.
|
|||||||
- Ein oder mehrere Racks gemeinsam anzeigen
|
- Ein oder mehrere Racks gemeinsam anzeigen
|
||||||
- Geräte positionsgenau anhand ihrer Höheneinheiten darstellen
|
- Geräte positionsgenau anhand ihrer Höheneinheiten darstellen
|
||||||
- Vorder- und Rückseite eines Racks getrennt betrachten
|
- Vorder- und Rückseite eines Racks getrennt betrachten
|
||||||
|
- Auf- und absteigende HE-Nummerierung sowie individuelle Start-HE automatisch berücksichtigen
|
||||||
- Rackübergreifende und externe Kabelverbindungen erkennen
|
- Rackübergreifende und externe Kabelverbindungen erkennen
|
||||||
- Geräte in der Kabeltopologie frei verschieben
|
- Geräte in der Kabeltopologie frei verschieben
|
||||||
|
- Geräte in der Kabeltopologie innerhalb jedes Racks nach ihrer Rackposition sortieren
|
||||||
|
- Kabeltopologie in den Browser-Vollbildmodus schalten
|
||||||
|
- Kabeltopologie zwischen 50 und 200 Prozent zoomen und im Vollbild scrollen
|
||||||
|
- Geräte in der Kabeltopologie mit rund 5 mm Abstand darstellen
|
||||||
|
- P2P-WirelessLinks und Multi-Access-WLANs als violette, gestrichelte Funkstrecken darstellen
|
||||||
- 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
|
||||||
@@ -22,6 +28,7 @@ Rückseite und Verkabelung.
|
|||||||
- Geräte und Kabel direkt aus der Topologie in NetBox öffnen
|
- Geräte und Kabel direkt aus der Topologie in NetBox öffnen
|
||||||
- Einzelne Racks, alle Racks oder die Kabeltopologie als SVG, PNG und draw.io exportieren
|
- Einzelne Racks, alle Racks oder die Kabeltopologie als SVG, PNG und draw.io exportieren
|
||||||
- Exporte grundsätzlich mit transparentem Hintergrund erzeugen
|
- Exporte grundsätzlich mit transparentem Hintergrund erzeugen
|
||||||
|
- Farben von Rackansicht und Export an den aktiven Light- oder Dark-Mode anpassen
|
||||||
- Topologie als PNG oder diagrams.net/draw.io-XML exportieren
|
- Topologie als PNG oder diagrams.net/draw.io-XML exportieren
|
||||||
- Zusätzliche Icons für Network-, Leaf-, Spine- und Aggregation-Switche
|
- Zusätzliche Icons für Network-, Leaf-, Spine- und Aggregation-Switche
|
||||||
- Unterstützung für helle und dunkle NetBox-Themes
|
- Unterstützung für helle und dunkle NetBox-Themes
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
font-size: .78rem;
|
font-size: .78rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
|
background: #eef6ff;
|
||||||
|
color: #132238;
|
||||||
}
|
}
|
||||||
.rack-device:hover {
|
.rack-device:hover {
|
||||||
min-height: 2.8rem;
|
min-height: 2.8rem;
|
||||||
@@ -13,10 +15,29 @@
|
|||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
.rack-device:hover span { white-space: normal; }
|
.rack-device:hover span { white-space: normal; }
|
||||||
|
.rack-frame {
|
||||||
|
border-color: #718096;
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: repeating-linear-gradient(to top, rgba(56, 73, 89, .16) 0, rgba(56, 73, 89, .16) 1px, transparent 1px, transparent calc(100% / var(--rack-units)));
|
||||||
|
}
|
||||||
|
.rack-device:hover { color: #132238; }
|
||||||
|
html[data-bs-theme="dark"] .rack-frame,
|
||||||
|
body[data-bs-theme="dark"] .rack-frame,
|
||||||
|
html[data-netbox-color-mode="dark"] .rack-frame {
|
||||||
|
border-color: #3b4654;
|
||||||
|
background-color: #212b36;
|
||||||
|
background-image: repeating-linear-gradient(to top, rgba(200, 215, 230, .14) 0, rgba(200, 215, 230, .14) 1px, transparent 1px, transparent calc(100% / var(--rack-units)));
|
||||||
|
}
|
||||||
|
html[data-bs-theme="dark"] .rack-device,
|
||||||
|
body[data-bs-theme="dark"] .rack-device,
|
||||||
|
html[data-netbox-color-mode="dark"] .rack-device {
|
||||||
|
background: #162e45;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
.rack-topology {
|
.rack-topology {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 18rem;
|
min-height: 18rem;
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: var(--tblr-bg-surface-secondary, rgba(127, 127, 127, .05));
|
background: var(--tblr-bg-surface-secondary, rgba(127, 127, 127, .05));
|
||||||
}
|
}
|
||||||
@@ -45,6 +66,7 @@
|
|||||||
width: 15rem;
|
width: 15rem;
|
||||||
}
|
}
|
||||||
.rack-topology-column h4 { text-align: center; font-size: .85rem; }
|
.rack-topology-column h4 { text-align: center; font-size: .85rem; }
|
||||||
|
.rack-topology-device-list { display: flex; flex-direction: column; gap: 40px; }
|
||||||
.rack-topology-node {
|
.rack-topology-node {
|
||||||
display: block;
|
display: block;
|
||||||
padding: .55rem .75rem;
|
padding: .55rem .75rem;
|
||||||
@@ -78,12 +100,27 @@
|
|||||||
transition: opacity .12s, stroke .12s, filter .12s, stroke-width .12s;
|
transition: opacity .12s, stroke .12s, filter .12s, stroke-width .12s;
|
||||||
}
|
}
|
||||||
.rack-topology-edge.is-highlighted {
|
.rack-topology-edge.is-highlighted {
|
||||||
stroke: #fff !important;
|
stroke: var(--topology-highlight-color, #16a34a) !important;
|
||||||
stroke-width: 5;
|
stroke-width: 5;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: drop-shadow(0 0 5px #fff);
|
filter: drop-shadow(0 0 5px var(--topology-highlight-color, #16a34a));
|
||||||
}
|
}
|
||||||
.rack-topology-edge.is-dimmed { opacity: .12; }
|
.rack-topology-edge.is-dimmed { opacity: .12; }
|
||||||
|
.rack-topology-edge.is-wireless {
|
||||||
|
stroke-dasharray: 7 7;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
#rack-topology-card:fullscreen {
|
||||||
|
margin: 0 !important;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--tblr-bg-surface, #fff);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#rack-topology-card:fullscreen .card-body { min-height: 0; flex: 1; overflow: hidden; }
|
||||||
|
#rack-topology-card:fullscreen .rack-topology { height: calc(100vh - 5.5rem); }
|
||||||
.rack-topology-tooltip {
|
.rack-topology-tooltip {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -109,3 +146,10 @@
|
|||||||
border-top: 1px solid var(--tblr-border-color, #ced4da);
|
border-top: 1px solid var(--tblr-border-color, #ced4da);
|
||||||
}
|
}
|
||||||
.rack-topology-tooltip small { opacity: .65; }
|
.rack-topology-tooltip small { opacity: .65; }
|
||||||
|
.rack-cable-count {
|
||||||
|
min-width: 2rem;
|
||||||
|
margin-left: .35rem;
|
||||||
|
background-color: #52606d !important;
|
||||||
|
color: #fff !important;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ html[data-netbox-color-mode=dark] #visgraph {
|
|||||||
.rack-frame { display: flex; height: calc(var(--rack-units) * 22px); min-height: 280px; border: 4px solid #3b4654; background: repeating-linear-gradient(to top, rgba(127,127,127,.14) 0, rgba(127,127,127,.14) 1px, transparent 1px, transparent calc(100% / var(--rack-units))); }
|
.rack-frame { display: flex; height: calc(var(--rack-units) * 22px); min-height: 280px; border: 4px solid #3b4654; background: repeating-linear-gradient(to top, rgba(127,127,127,.14) 0, rgba(127,127,127,.14) 1px, transparent 1px, transparent calc(100% / var(--rack-units))); }
|
||||||
.rack-unit-labels { width: 2rem; display: flex; flex-direction: column; justify-content: space-around; font-size: .55rem; text-align: center; color: var(--tblr-secondary, #667382); }
|
.rack-unit-labels { width: 2rem; display: flex; flex-direction: column; justify-content: space-around; font-size: .55rem; text-align: center; color: var(--tblr-secondary, #667382); }
|
||||||
.rack-device-area { position: relative; flex: 1; border-left: 1px solid #3b4654; }
|
.rack-device-area { position: relative; flex: 1; border-left: 1px solid #3b4654; }
|
||||||
.rack-device { position: absolute; left: 3px; right: 3px; min-height: 2px; overflow: hidden; padding: 2px 6px; border: 2px solid #1685fc; border-radius: 3px; background: #162e45; color: #fff; font-size: .78rem; line-height: 1.15; text-decoration: none; }
|
.rack-device { position: absolute; left: 3px; right: 3px; min-height: 2px; overflow: hidden; padding: 2px 6px; border: 2px solid #1685fc; border-radius: 3px; background: #eef6ff; color: #132238; font-size: .78rem; line-height: 1.15; text-decoration: none; }
|
||||||
.rack-device:hover { min-height: 2.8rem; overflow: visible; filter: brightness(1.25); color: #fff; z-index: 3; }
|
.rack-device:hover { min-height: 2.8rem; overflow: visible; filter: brightness(1.25); color: #fff; z-index: 3; }
|
||||||
.rack-device:hover span { white-space: normal; }
|
.rack-device:hover span { white-space: normal; }
|
||||||
.rack-device span, .rack-device small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.rack-device span, .rack-device small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.rack-device small { font-size: .6rem; opacity: .8; }
|
.rack-device small { font-size: .6rem; opacity: .8; }
|
||||||
.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 { position: relative; min-height: 18rem; overflow: 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-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-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: 15rem; }
|
.rack-topology-column { display: flex; flex-direction: column; gap: .75rem; width: 15rem; }
|
||||||
@@ -62,7 +62,7 @@ html[data-netbox-color-mode=dark] #visgraph {
|
|||||||
.rack-topology-node strong, .rack-topology-node small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.rack-topology-node strong, .rack-topology-node small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.rack-topology-node small { opacity: .7; }
|
.rack-topology-node small { opacity: .7; }
|
||||||
.rack-topology-edge { fill: none; stroke-width: 3; opacity: .8; pointer-events: stroke; cursor: pointer; transition: opacity .12s, stroke .12s, filter .12s, stroke-width .12s; }
|
.rack-topology-edge { fill: none; stroke-width: 3; opacity: .8; pointer-events: stroke; cursor: pointer; transition: opacity .12s, stroke .12s, filter .12s, stroke-width .12s; }
|
||||||
.rack-topology-edge.is-highlighted { stroke: #fff !important; stroke-width: 5; opacity: 1; filter: drop-shadow(0 0 5px #fff); }
|
.rack-topology-edge.is-highlighted { stroke: var(--topology-highlight-color, #16a34a) !important; stroke-width: 5; opacity: 1; filter: drop-shadow(0 0 5px var(--topology-highlight-color, #16a34a)); }
|
||||||
.rack-topology-edge.is-dimmed { opacity: .12; }
|
.rack-topology-edge.is-dimmed { opacity: .12; }
|
||||||
.rack-topology-tooltip { display: none; position: absolute; z-index: 10; width: max-content; max-width: 28rem; padding: .65rem .8rem; border: 1px solid var(--tblr-border-color, #ced4da); border-radius: .35rem; background: var(--tblr-bg-surface, #fff); box-shadow: 0 4px 18px rgba(0,0,0,.25); pointer-events: none; }
|
.rack-topology-tooltip { display: none; position: absolute; z-index: 10; width: max-content; max-width: 28rem; padding: .65rem .8rem; border: 1px solid var(--tblr-border-color, #ced4da); border-radius: .35rem; background: var(--tblr-bg-surface, #fff); box-shadow: 0 4px 18px rgba(0,0,0,.25); pointer-events: none; }
|
||||||
.rack-topology-tooltip.is-visible { display: block; }
|
.rack-topology-tooltip.is-visible { display: block; }
|
||||||
@@ -70,6 +70,23 @@ html[data-netbox-color-mode=dark] #visgraph {
|
|||||||
.rack-topology-tooltip ul { margin: .4rem 0 0; padding: 0; list-style: none; }
|
.rack-topology-tooltip ul { margin: .4rem 0 0; padding: 0; list-style: none; }
|
||||||
.rack-topology-tooltip li + li { margin-top: .45rem; padding-top: .45rem; border-top: 1px solid var(--tblr-border-color, #ced4da); }
|
.rack-topology-tooltip li + li { margin-top: .45rem; padding-top: .45rem; border-top: 1px solid var(--tblr-border-color, #ced4da); }
|
||||||
.rack-topology-tooltip small { opacity: .65; }
|
.rack-topology-tooltip small { opacity: .65; }
|
||||||
|
.rack-cable-count { min-width: 2rem; margin-left: .35rem; background-color: #52606d !important; color: #fff !important; text-align: center; }
|
||||||
|
|
||||||
|
.rack-frame {
|
||||||
|
border-color: #718096;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rack-topology-device-list { display: flex; flex-direction: column; gap: 40px; }
|
||||||
|
.rack-topology-edge.is-wireless { stroke-dasharray: 7 7; stroke-linecap: round; }
|
||||||
|
#rack-topology-card:fullscreen { margin: 0 !important; border: 0; border-radius: 0; background: var(--tblr-bg-surface, #fff); display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
#rack-topology-card:fullscreen .card-body { min-height: 0; flex: 1; overflow: hidden; }
|
||||||
|
#rack-topology-card:fullscreen .rack-topology { height: calc(100vh - 5.5rem); }
|
||||||
|
|
||||||
|
html[data-bs-theme="dark"], body[data-bs-theme="dark"], html[data-netbox-color-mode="dark"] {
|
||||||
|
.rack-frame { border-color: #3b4654; background-color: #212b36; }
|
||||||
|
.rack-device { background: #162e45; color: #fff; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.rack-elevation-grid { grid-template-columns: 1fr; }
|
.rack-elevation-grid { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-4">
|
<div id="rack-topology-card" class="card mt-4">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<strong>Kabeltopologie</strong>
|
<strong>Kabeltopologie</strong>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
@@ -91,9 +91,17 @@
|
|||||||
<button class="btn btn-sm btn-outline-primary topology-export" data-format="png">PNG</button>
|
<button class="btn btn-sm btn-outline-primary topology-export" data-format="png">PNG</button>
|
||||||
<button class="btn btn-sm btn-outline-primary topology-export" data-format="drawio">draw.io</button>
|
<button class="btn btn-sm btn-outline-primary topology-export" data-format="drawio">draw.io</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="btn-group" aria-label="Topologie zoomen">
|
||||||
|
<button id="rack-topology-zoom-out" class="btn btn-sm btn-outline-secondary" type="button" title="Verkleinern"><i class="mdi mdi-minus"></i></button>
|
||||||
|
<button id="rack-topology-zoom-reset" class="btn btn-sm btn-outline-secondary" type="button" title="Zoom zurücksetzen"><span id="rack-topology-zoom-label">100%</span></button>
|
||||||
|
<button id="rack-topology-zoom-in" class="btn btn-sm btn-outline-secondary" type="button" title="Vergrößern"><i class="mdi mdi-plus"></i></button>
|
||||||
|
</div>
|
||||||
<button id="rack-topology-reset" class="btn btn-sm btn-outline-secondary" type="button">
|
<button id="rack-topology-reset" class="btn btn-sm btn-outline-secondary" type="button">
|
||||||
<i class="mdi mdi-restore"></i> Positionen zurücksetzen
|
<i class="mdi mdi-restore"></i> Positionen zurücksetzen
|
||||||
</button>
|
</button>
|
||||||
|
<button id="rack-topology-fullscreen" class="btn btn-sm btn-outline-secondary" type="button">
|
||||||
|
<i class="mdi mdi-fullscreen"></i> Vollbild
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -108,7 +116,7 @@
|
|||||||
{{ rack_export|json_script:"rack-export-data" }}
|
{{ rack_export|json_script:"rack-export-data" }}
|
||||||
|
|
||||||
<div class="card mt-4">
|
<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="card-header"><strong>Verkabelung der angezeigten Racks</strong> <span class="badge rack-cable-count">{{ cables|length }}</span></div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead><tr><th>Kabel</th><th>A-Seite</th><th>B-Seite</th><th>Typ</th><th>Status</th></tr></thead>
|
<thead><tr><th>Kabel</th><th>A-Seite</th><th>B-Seite</th><th>Typ</th><th>Status</th></tr></thead>
|
||||||
@@ -139,29 +147,77 @@
|
|||||||
const source = document.getElementById('rack-topology-data');
|
const source = document.getElementById('rack-topology-data');
|
||||||
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 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');
|
||||||
const tooltip = root.querySelector('.rack-topology-tooltip');
|
const tooltip = root.querySelector('.rack-topology-tooltip');
|
||||||
const resetButton = document.getElementById('rack-topology-reset');
|
const resetButton = document.getElementById('rack-topology-reset');
|
||||||
const storageKey = `netbox-rack-topology:${window.location.pathname}${window.location.search}`;
|
const fullscreenButton = document.getElementById('rack-topology-fullscreen');
|
||||||
|
const topologyCard = document.getElementById('rack-topology-card');
|
||||||
|
const zoomInButton = document.getElementById('rack-topology-zoom-in');
|
||||||
|
const zoomOutButton = document.getElementById('rack-topology-zoom-out');
|
||||||
|
const zoomResetButton = document.getElementById('rack-topology-zoom-reset');
|
||||||
|
const zoomLabel = document.getElementById('rack-topology-zoom-label');
|
||||||
|
let topologyZoom = 1;
|
||||||
|
const activeNetBoxTheme = () => {
|
||||||
|
for (const element of [topologyCard, document.body, document.documentElement]) {
|
||||||
|
const match = getComputedStyle(element).backgroundColor.match(/[\d.]+/g);
|
||||||
|
if (match && match.length >= 3 && (match.length < 4 || Number(match[3]) > 0)) {
|
||||||
|
const [red, green, blue] = match.map(Number);
|
||||||
|
return (.2126 * red + .7152 * green + .0722 * blue) < 140 ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return document.body.dataset.bsTheme || document.documentElement.dataset.bsTheme || 'light';
|
||||||
|
};
|
||||||
|
const topologyHighlightColor = () => activeNetBoxTheme() === 'dark' ? '#ffffff' : '#16a34a';
|
||||||
|
const applyTopologyTheme = () => {
|
||||||
|
root.style.setProperty('--topology-highlight-color', topologyHighlightColor());
|
||||||
|
root.querySelectorAll('.rack-topology-edge.is-highlighted').forEach(path => {
|
||||||
|
path.style.setProperty('stroke', topologyHighlightColor(), 'important');
|
||||||
|
path.style.setProperty('filter', `drop-shadow(0 0 5px ${topologyHighlightColor()})`, 'important');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
applyTopologyTheme();
|
||||||
|
const topologyThemeObserver = new MutationObserver(() => requestAnimationFrame(applyTopologyTheme));
|
||||||
|
topologyThemeObserver.observe(document.body, {attributes: true, attributeFilter: ['data-bs-theme']});
|
||||||
|
topologyThemeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-bs-theme', 'data-netbox-color-mode'],
|
||||||
|
});
|
||||||
|
const storageKey = `netbox-rack-topology-v3:${window.location.pathname}${window.location.search}`;
|
||||||
let storedPositions = {};
|
let storedPositions = {};
|
||||||
try { storedPositions = JSON.parse(localStorage.getItem(storageKey) || '{}'); } catch (_) {}
|
try { storedPositions = JSON.parse(localStorage.getItem(storageKey) || '{}'); } catch (_) {}
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
|
|
||||||
data.nodes.forEach(node => {
|
data.nodes.forEach(node => {
|
||||||
const groupName = node.rack_name || 'Extern';
|
const groupKey = node.rack_id ? `rack-${node.rack_id}` : `external-${node.rack_name || 'Extern'}`;
|
||||||
if (!groups.has(groupName)) groups.set(groupName, []);
|
if (!groups.has(groupKey)) groups.set(groupKey, {
|
||||||
groups.get(groupName).push(node);
|
name: node.rack_name || 'Extern',
|
||||||
|
rackUnits: node.rack_u_height || null,
|
||||||
|
descUnits: Boolean(node.rack_desc_units),
|
||||||
|
nodes: [],
|
||||||
|
});
|
||||||
|
groups.get(groupKey).nodes.push(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
groups.forEach((nodes, name) => {
|
groups.forEach(group => {
|
||||||
const column = document.createElement('section');
|
const column = document.createElement('section');
|
||||||
column.className = 'rack-topology-column';
|
column.className = 'rack-topology-column';
|
||||||
const heading = document.createElement('h4');
|
const heading = document.createElement('h4');
|
||||||
heading.textContent = name;
|
heading.textContent = group.name;
|
||||||
column.appendChild(heading);
|
column.appendChild(heading);
|
||||||
nodes.forEach(node => {
|
const nodeContainer = document.createElement('div');
|
||||||
|
nodeContainer.className = 'rack-topology-device-list';
|
||||||
|
nodeContainer.style.display = 'flex';
|
||||||
|
nodeContainer.style.flexDirection = 'column';
|
||||||
|
nodeContainer.style.gap = '20px';
|
||||||
|
nodeContainer.style.rowGap = '20px';
|
||||||
|
column.appendChild(nodeContainer);
|
||||||
|
group.nodes.sort((a, b) => group.descUnits
|
||||||
|
? (a.position || 0) - (b.position || 0)
|
||||||
|
: (b.position || 0) - (a.position || 0)
|
||||||
|
).forEach(node => {
|
||||||
const item = document.createElement(node.url ? 'a' : 'div');
|
const item = document.createElement(node.url ? 'a' : 'div');
|
||||||
item.className = 'rack-topology-node';
|
item.className = 'rack-topology-node';
|
||||||
item.id = `topology-${node.node_id}`;
|
item.id = `topology-${node.node_id}`;
|
||||||
@@ -171,11 +227,11 @@
|
|||||||
const nameElement = document.createElement('strong');
|
const nameElement = document.createElement('strong');
|
||||||
nameElement.textContent = node.device_name;
|
nameElement.textContent = node.device_name;
|
||||||
const portElement = document.createElement('small');
|
const portElement = document.createElement('small');
|
||||||
portElement.textContent = node.name;
|
portElement.textContent = node.subtitle || node.name;
|
||||||
item.append(nameElement, portElement);
|
item.append(nameElement, portElement);
|
||||||
const saved = storedPositions[node.node_id];
|
const saved = storedPositions[node.node_id];
|
||||||
if (saved) item.style.transform = `translate(${saved.x}px, ${saved.y}px)`;
|
if (saved) item.style.transform = `translate(${saved.x}px, ${saved.y}px)`;
|
||||||
column.appendChild(item);
|
nodeContainer.appendChild(item);
|
||||||
});
|
});
|
||||||
columns.appendChild(column);
|
columns.appendChild(column);
|
||||||
});
|
});
|
||||||
@@ -194,11 +250,21 @@
|
|||||||
element.textContent = value == null ? '—' : String(value);
|
element.textContent = value == null ? '—' : String(value);
|
||||||
return element.innerHTML;
|
return element.innerHTML;
|
||||||
};
|
};
|
||||||
|
const setEdgeHighlighted = (path, highlighted) => {
|
||||||
|
path.classList.toggle('is-highlighted', highlighted);
|
||||||
|
if (highlighted) {
|
||||||
|
path.style.setProperty('stroke', topologyHighlightColor(), 'important');
|
||||||
|
path.style.setProperty('filter', `drop-shadow(0 0 5px ${topologyHighlightColor()})`, 'important');
|
||||||
|
} else {
|
||||||
|
path.style.removeProperty('stroke');
|
||||||
|
path.style.removeProperty('filter');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const drawEdges = () => {
|
const drawEdges = () => {
|
||||||
const bounds = root.getBoundingClientRect();
|
const bounds = root.getBoundingClientRect();
|
||||||
let width = Math.max(root.clientWidth, columns.scrollWidth + 32);
|
let width = Math.max(root.clientWidth, columns.scrollWidth * topologyZoom + 32);
|
||||||
let height = Math.max(root.clientHeight, columns.scrollHeight + 32);
|
let height = Math.max(root.clientHeight, columns.scrollHeight * topologyZoom + 32);
|
||||||
svg.style.width = `${width}px`;
|
svg.style.width = `${width}px`;
|
||||||
svg.style.height = `${height}px`;
|
svg.style.height = `${height}px`;
|
||||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||||
@@ -208,15 +274,16 @@
|
|||||||
const to = document.getElementById(`topology-${edge.to}`);
|
const to = document.getElementById(`topology-${edge.to}`);
|
||||||
if (!from || !to) return;
|
if (!from || !to) return;
|
||||||
const a = from.getBoundingClientRect(), b = to.getBoundingClientRect();
|
const a = from.getBoundingClientRect(), b = to.getBoundingClientRect();
|
||||||
const x1 = a.left + a.width / 2 - bounds.left;
|
const x1 = a.left + a.width / 2 - bounds.left + root.scrollLeft;
|
||||||
const y1 = a.top + a.height / 2 - bounds.top;
|
const y1 = a.top + a.height / 2 - bounds.top + root.scrollTop;
|
||||||
const x2 = b.left + b.width / 2 - bounds.left;
|
const x2 = b.left + b.width / 2 - bounds.left + root.scrollLeft;
|
||||||
const y2 = b.top + b.height / 2 - bounds.top;
|
const y2 = b.top + b.height / 2 - bounds.top + root.scrollTop;
|
||||||
const bend = Math.max(30, Math.abs(x2 - x1) * .35);
|
const bend = Math.max(30, Math.abs(x2 - x1) * .35);
|
||||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
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('d', `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`);
|
||||||
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');
|
||||||
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');
|
||||||
@@ -224,16 +291,16 @@
|
|||||||
path.appendChild(title);
|
path.appendChild(title);
|
||||||
if (edge.url) path.addEventListener('click', () => window.location.href = edge.url);
|
if (edge.url) path.addEventListener('click', () => window.location.href = edge.url);
|
||||||
path.addEventListener('mouseenter', event => {
|
path.addEventListener('mouseenter', event => {
|
||||||
path.classList.add('is-highlighted');
|
setEdgeHighlighted(path, true);
|
||||||
showTooltip(
|
showTooltip(
|
||||||
`<strong>${escapeHtml(edge.label)}</strong>` +
|
`<strong>${isWirelessEdge(edge) ? 'WLAN · ' : ''}${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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
path.addEventListener('mouseleave', () => {
|
path.addEventListener('mouseleave', () => {
|
||||||
path.classList.remove('is-highlighted');
|
setEdgeHighlighted(path, false);
|
||||||
hideTooltip();
|
hideTooltip();
|
||||||
});
|
});
|
||||||
svg.appendChild(path);
|
svg.appendChild(path);
|
||||||
@@ -246,20 +313,24 @@
|
|||||||
node.addEventListener('mouseenter', () => {
|
node.addEventListener('mouseenter', () => {
|
||||||
const connections = connectedEdges(nodeId);
|
const connections = connectedEdges(nodeId);
|
||||||
svg.querySelectorAll('.rack-topology-edge').forEach(path => {
|
svg.querySelectorAll('.rack-topology-edge').forEach(path => {
|
||||||
path.classList.toggle('is-highlighted', path.dataset.from === nodeId || path.dataset.to === nodeId);
|
const connected = path.dataset.from === nodeId || path.dataset.to === nodeId;
|
||||||
path.classList.toggle('is-dimmed', path.dataset.from !== nodeId && path.dataset.to !== nodeId);
|
setEdgeHighlighted(path, connected);
|
||||||
|
path.classList.toggle('is-dimmed', !connected);
|
||||||
});
|
});
|
||||||
const rows = connections.map(edge => {
|
const rows = connections.map(edge => {
|
||||||
const outgoing = edge.from === nodeId;
|
const outgoing = edge.from === nodeId;
|
||||||
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>${escapeHtml(edge.label)}</small></li>`;
|
return `<li><strong>${escapeHtml(localPort)}</strong><span>→ ${escapeHtml(remoteDevice)} · ${escapeHtml(remotePort)}</span><small>${isWirelessEdge(edge) ? 'WLAN · ' : ''}${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);
|
||||||
});
|
});
|
||||||
node.addEventListener('mouseleave', () => {
|
node.addEventListener('mouseleave', () => {
|
||||||
svg.querySelectorAll('.rack-topology-edge').forEach(path => path.classList.remove('is-highlighted', 'is-dimmed'));
|
svg.querySelectorAll('.rack-topology-edge').forEach(path => {
|
||||||
|
setEdgeHighlighted(path, false);
|
||||||
|
path.classList.remove('is-dimmed');
|
||||||
|
});
|
||||||
hideTooltip();
|
hideTooltip();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,8 +345,8 @@
|
|||||||
});
|
});
|
||||||
node.addEventListener('pointermove', event => {
|
node.addEventListener('pointermove', event => {
|
||||||
if (!drag) return;
|
if (!drag) return;
|
||||||
const x = drag.x + event.clientX - drag.startX;
|
const x = drag.x + (event.clientX - drag.startX) / topologyZoom;
|
||||||
const y = drag.y + event.clientY - drag.startY;
|
const y = drag.y + (event.clientY - drag.startY) / topologyZoom;
|
||||||
drag.moved ||= Math.abs(x - drag.x) > 3 || Math.abs(y - drag.y) > 3;
|
drag.moved ||= Math.abs(x - drag.x) > 3 || Math.abs(y - drag.y) > 3;
|
||||||
node.style.transform = `translate(${x}px, ${y}px)`;
|
node.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
storedPositions[nodeId] = {x, y};
|
storedPositions[nodeId] = {x, y};
|
||||||
@@ -304,11 +375,42 @@
|
|||||||
root.querySelectorAll('.rack-topology-node').forEach(node => node.style.transform = '');
|
root.querySelectorAll('.rack-topology-node').forEach(node => node.style.transform = '');
|
||||||
drawEdges();
|
drawEdges();
|
||||||
});
|
});
|
||||||
|
fullscreenButton?.addEventListener('click', async () => {
|
||||||
|
if (document.fullscreenElement === topologyCard) await document.exitFullscreen();
|
||||||
|
else await topologyCard.requestFullscreen();
|
||||||
|
});
|
||||||
|
document.addEventListener('fullscreenchange', () => {
|
||||||
|
const active = document.fullscreenElement === topologyCard;
|
||||||
|
fullscreenButton.innerHTML = active
|
||||||
|
? '<i class="mdi mdi-fullscreen-exit"></i> Vollbild verlassen'
|
||||||
|
: '<i class="mdi mdi-fullscreen"></i> Vollbild';
|
||||||
|
requestAnimationFrame(drawEdges);
|
||||||
|
});
|
||||||
|
const setTopologyZoom = value => {
|
||||||
|
topologyZoom = Math.min(2, Math.max(.5, Math.round(value * 10) / 10));
|
||||||
|
columns.style.zoom = topologyZoom;
|
||||||
|
zoomLabel.textContent = `${Math.round(topologyZoom * 100)}%`;
|
||||||
|
requestAnimationFrame(drawEdges);
|
||||||
|
};
|
||||||
|
zoomInButton?.addEventListener('click', () => setTopologyZoom(topologyZoom + .1));
|
||||||
|
zoomOutButton?.addEventListener('click', () => setTopologyZoom(topologyZoom - .1));
|
||||||
|
zoomResetButton?.addEventListener('click', () => setTopologyZoom(1));
|
||||||
|
|
||||||
const xmlEscape = value => String(value ?? '').replace(/[&<>"']/g, char => ({
|
const xmlEscape = value => String(value ?? '').replace(/[&<>"']/g, char => ({
|
||||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
})[char]);
|
})[char]);
|
||||||
const safeName = value => String(value || 'export').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-|-$/g, '');
|
const safeName = value => String(value || 'export').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-|-$/g, '');
|
||||||
|
const exportPalette = () => {
|
||||||
|
const declaredTheme = activeNetBoxTheme();
|
||||||
|
const dark = declaredTheme === 'dark' || (!declaredTheme && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
return dark ? {
|
||||||
|
frame: '#52606d', grid: '#667382', heading: '#8fc8ff', muted: '#a9b7c6',
|
||||||
|
deviceFill: '#162e45', deviceText: '#ffffff', deviceSubtext: '#cad5df',
|
||||||
|
} : {
|
||||||
|
frame: '#718096', grid: '#8b99a8', heading: '#0b69b7', muted: '#52606d',
|
||||||
|
deviceFill: '#eef6ff', deviceText: '#132238', deviceSubtext: '#52606d',
|
||||||
|
};
|
||||||
|
};
|
||||||
const svgDocument = (width, height, content) =>
|
const svgDocument = (width, height, content) =>
|
||||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${content}</svg>`;
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${content}</svg>`;
|
||||||
const drawioDocument = cells =>
|
const drawioDocument = cells =>
|
||||||
@@ -321,11 +423,16 @@
|
|||||||
const devices = Array.from(card.querySelectorAll('.rack-device')).map(device => {
|
const devices = Array.from(card.querySelectorAll('.rack-device')).map(device => {
|
||||||
const bottom = Number.parseFloat(device.style.bottom) || 0;
|
const bottom = Number.parseFloat(device.style.bottom) || 0;
|
||||||
const height = Number.parseFloat(device.style.height) || (100 / rackUnits);
|
const height = Number.parseFloat(device.style.height) || (100 / rackUnits);
|
||||||
|
const deviceHeight = Math.max(height / 100 * rackUnits, 1);
|
||||||
|
const bottomUnits = bottom / 100 * rackUnits;
|
||||||
|
const startingUnit = Number(rack.starting_unit) || 1;
|
||||||
return {
|
return {
|
||||||
name: device.dataset.deviceName || device.querySelector('span')?.textContent || 'Gerät',
|
name: device.dataset.deviceName || device.querySelector('span')?.textContent || 'Gerät',
|
||||||
type: device.dataset.deviceType || '',
|
type: device.dataset.deviceType || '',
|
||||||
position: bottom / 100 * rackUnits + 1,
|
position: rack.desc_units
|
||||||
height: Math.max(height / 100 * rackUnits, 1),
|
? startingUnit + rackUnits - deviceHeight - bottomUnits
|
||||||
|
: startingUnit + bottomUnits,
|
||||||
|
height: deviceHeight,
|
||||||
face: device.dataset.face === 'rear' ? 'rear' : 'front',
|
face: device.dataset.face === 'rear' ? 'rear' : 'front',
|
||||||
color: device.dataset.deviceColor || '#1685fc',
|
color: device.dataset.deviceColor || '#1685fc',
|
||||||
};
|
};
|
||||||
@@ -334,6 +441,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const buildRackDrawio = racks => {
|
const buildRackDrawio = racks => {
|
||||||
|
const palette = exportPalette();
|
||||||
const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55;
|
const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55;
|
||||||
const rackWidth = labelWidth + faceWidth * 2 + faceGap;
|
const rackWidth = labelWidth + faceWidth * 2 + faceGap;
|
||||||
let cells = '', cellId = 2;
|
let cells = '', cellId = 2;
|
||||||
@@ -345,21 +453,26 @@
|
|||||||
racks.forEach((rack, rackIndex) => {
|
racks.forEach((rack, rackIndex) => {
|
||||||
const baseX = 10 + rackIndex * (rackWidth + rackGap);
|
const baseX = 10 + rackIndex * (rackWidth + rackGap);
|
||||||
const rackHeight = rack.u_height * unit;
|
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);
|
const startingUnit = Number(rack.starting_unit) || 1;
|
||||||
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);
|
vertex(rack.name, `text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=16;fontStyle=1;fontColor=${palette.heading};`, 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=${palette.muted};`, baseX, 24, rackWidth, 20);
|
||||||
['front', 'rear'].forEach((face, faceIndex) => {
|
['front', 'rear'].forEach((face, faceIndex) => {
|
||||||
const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap);
|
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(face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE', `text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=10;fontColor=${palette.muted};`, x, 38, faceWidth, 17);
|
||||||
vertex('', 'rounded=0;html=1;fillColor=none;strokeColor=#52606d;strokeWidth=4;', x, header, faceWidth, rackHeight);
|
vertex('', `rounded=0;html=1;fillColor=none;strokeColor=${palette.frame};strokeWidth=4;`, x, header, faceWidth, rackHeight);
|
||||||
for (let u = 1; u <= rack.u_height; u++) {
|
for (let row = 0; row < rack.u_height; row++) {
|
||||||
const y = header + rackHeight - u * unit;
|
const y = header + row * 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);
|
const unitLabel = rack.desc_units ? startingUnit + row : startingUnit + rack.u_height - 1 - row;
|
||||||
|
if (faceIndex === 0) vertex(String(unitLabel), `text;html=1;strokeColor=none;fillColor=none;align=right;fontSize=8;fontColor=${palette.muted};`, x - labelWidth, y, labelWidth - 4, unit);
|
||||||
}
|
}
|
||||||
rack.devices.filter(device => device.face === face).forEach(device => {
|
rack.devices.filter(device => device.face === face).forEach(device => {
|
||||||
const y = header + rackHeight - (device.position - 1 + device.height) * unit;
|
const positionOffset = device.position - startingUnit;
|
||||||
|
const y = rack.desc_units
|
||||||
|
? header + positionOffset * unit
|
||||||
|
: header + rackHeight - (positionOffset + device.height) * unit;
|
||||||
const h = Math.max(device.height * unit, 4);
|
const h = Math.max(device.height * unit, 4);
|
||||||
const value = h >= 31 ? `${device.name}<br><font style="font-size:9px">U${device.position} · ${device.type}</font>` : device.name;
|
const value = h >= 31 ? `${device.name}<br><font style="font-size:9px">U${device.position} · ${device.type}</font>` : 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);
|
vertex(value, `rounded=1;html=1;whiteSpace=wrap;overflow=hidden;align=left;verticalAlign=top;spacingLeft=5;spacingTop=2;fillColor=${palette.deviceFill};strokeColor=${device.color};strokeWidth=2;fontColor=${palette.deviceText};fontSize=11;`, x + 3, y + 1, faceWidth - 6, h - 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -367,6 +480,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildRackSvg = racks => {
|
const buildRackSvg = racks => {
|
||||||
|
const palette = exportPalette();
|
||||||
const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55;
|
const unit = 22, faceWidth = 250, labelWidth = 28, faceGap = 28, rackGap = 44, header = 55;
|
||||||
const rackWidth = labelWidth + faceWidth * 2 + faceGap;
|
const rackWidth = labelWidth + faceWidth * 2 + faceGap;
|
||||||
const maxUnits = Math.max(...racks.map(rack => rack.u_height || 1), 1);
|
const maxUnits = Math.max(...racks.map(rack => rack.u_height || 1), 1);
|
||||||
@@ -376,23 +490,28 @@
|
|||||||
racks.forEach((rack, rackIndex) => {
|
racks.forEach((rack, rackIndex) => {
|
||||||
const baseX = 10 + rackIndex * (rackWidth + rackGap);
|
const baseX = 10 + rackIndex * (rackWidth + rackGap);
|
||||||
const rackHeight = rack.u_height * unit;
|
const rackHeight = rack.u_height * unit;
|
||||||
content += `<text x="${baseX}" y="20" fill="#1685fc" font-family="sans-serif" font-size="16" font-weight="700">${xmlEscape(rack.name)}</text>`;
|
const startingUnit = Number(rack.starting_unit) || 1;
|
||||||
content += `<text x="${baseX}" y="39" fill="#778899" font-family="sans-serif" font-size="11">${xmlEscape(rack.site)}${rack.location ? ` · ${xmlEscape(rack.location)}` : ''} · ${rack.u_height}U</text>`;
|
content += `<text x="${baseX}" y="20" fill="${palette.heading}" font-family="sans-serif" font-size="16" font-weight="700">${xmlEscape(rack.name)}</text>`;
|
||||||
|
content += `<text x="${baseX}" y="39" fill="${palette.muted}" font-family="sans-serif" font-size="11">${xmlEscape(rack.site)}${rack.location ? ` · ${xmlEscape(rack.location)}` : ''} · ${rack.u_height}U</text>`;
|
||||||
['front', 'rear'].forEach((face, faceIndex) => {
|
['front', 'rear'].forEach((face, faceIndex) => {
|
||||||
const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap);
|
const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap);
|
||||||
content += `<text x="${x + faceWidth / 2}" y="51" text-anchor="middle" fill="#778899" font-family="sans-serif" font-size="10">${face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE'}</text>`;
|
content += `<text x="${x + faceWidth / 2}" y="51" text-anchor="middle" fill="${palette.muted}" font-family="sans-serif" font-size="10">${face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE'}</text>`;
|
||||||
content += `<rect x="${x}" y="${header}" width="${faceWidth}" height="${rackHeight}" fill="none" stroke="#52606d" stroke-width="4"/>`;
|
content += `<rect x="${x}" y="${header}" width="${faceWidth}" height="${rackHeight}" fill="none" stroke="${palette.frame}" stroke-width="4"/>`;
|
||||||
for (let u = 1; u <= rack.u_height; u++) {
|
for (let row = 0; row < rack.u_height; row++) {
|
||||||
const y = header + rackHeight - u * unit;
|
const y = header + row * unit;
|
||||||
content += `<line x1="${x}" y1="${y}" x2="${x + faceWidth}" y2="${y}" stroke="#667382" stroke-opacity=".35"/>`;
|
const unitLabel = rack.desc_units ? startingUnit + row : startingUnit + rack.u_height - 1 - row;
|
||||||
if (faceIndex === 0) content += `<text x="${x - 6}" y="${y + 15}" text-anchor="end" fill="#778899" font-family="sans-serif" font-size="9">${u}</text>`;
|
content += `<line x1="${x}" y1="${y}" x2="${x + faceWidth}" y2="${y}" stroke="${palette.grid}" stroke-opacity=".35"/>`;
|
||||||
|
if (faceIndex === 0) content += `<text x="${x - 6}" y="${y + 15}" text-anchor="end" fill="${palette.muted}" font-family="sans-serif" font-size="9">${unitLabel}</text>`;
|
||||||
}
|
}
|
||||||
rack.devices.filter(device => device.face === face).forEach(device => {
|
rack.devices.filter(device => device.face === face).forEach(device => {
|
||||||
const y = header + rackHeight - (device.position - 1 + device.height) * unit;
|
const positionOffset = device.position - startingUnit;
|
||||||
|
const y = rack.desc_units
|
||||||
|
? header + positionOffset * unit
|
||||||
|
: header + rackHeight - (positionOffset + device.height) * unit;
|
||||||
const h = Math.max(device.height * unit, 4);
|
const h = Math.max(device.height * unit, 4);
|
||||||
content += `<rect x="${x + 3}" y="${y + 1}" width="${faceWidth - 6}" height="${h - 2}" rx="3" fill="#162e45" stroke="${xmlEscape(device.color)}" stroke-width="2"/>`;
|
content += `<rect x="${x + 3}" y="${y + 1}" width="${faceWidth - 6}" height="${h - 2}" rx="3" fill="${palette.deviceFill}" stroke="${xmlEscape(device.color)}" stroke-width="2"/>`;
|
||||||
if (h >= 13) content += `<text x="${x + 9}" y="${y + Math.min(15, h - 4)}" fill="#fff" font-family="sans-serif" font-size="11" font-weight="600">${xmlEscape(device.name.slice(0, 34))}</text>`;
|
if (h >= 13) content += `<text x="${x + 9}" y="${y + Math.min(15, h - 4)}" fill="${palette.deviceText}" font-family="sans-serif" font-size="11" font-weight="600">${xmlEscape(device.name.slice(0, 34))}</text>`;
|
||||||
if (h >= 31) content += `<text x="${x + 9}" y="${y + 28}" fill="#cad5df" font-family="sans-serif" font-size="9">U${device.position} · ${xmlEscape(device.type.slice(0, 34))}</text>`;
|
if (h >= 31) content += `<text x="${x + 9}" y="${y + 28}" fill="${palette.deviceSubtext}" font-family="sans-serif" font-size="9">U${device.position} · ${xmlEscape(device.type.slice(0, 34))}</text>`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -400,13 +519,22 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const buildTopologySvg = () => {
|
const buildTopologySvg = () => {
|
||||||
|
const palette = exportPalette();
|
||||||
const rootBounds = root.getBoundingClientRect();
|
const rootBounds = root.getBoundingClientRect();
|
||||||
let width = Math.max(root.clientWidth, columns.scrollWidth + 32);
|
let width = Math.max(root.clientWidth, columns.scrollWidth * topologyZoom + 32);
|
||||||
let height = Math.max(root.clientHeight, columns.scrollHeight + 32);
|
let height = Math.max(root.clientHeight, columns.scrollHeight * topologyZoom + 32);
|
||||||
const positions = new Map();
|
const positions = new Map();
|
||||||
const nodeCells = new Map();
|
const nodeCells = new Map();
|
||||||
let content = '';
|
let content = '';
|
||||||
let drawioCells = '', drawioId = 2;
|
let drawioCells = '', drawioId = 2;
|
||||||
|
root.querySelectorAll('.rack-topology-column').forEach(column => {
|
||||||
|
const heading = column.querySelector('h4');
|
||||||
|
const headingBounds = heading.getBoundingClientRect();
|
||||||
|
const headingX = headingBounds.left - rootBounds.left + root.scrollLeft;
|
||||||
|
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>`;
|
||||||
|
});
|
||||||
root.querySelectorAll('.rack-topology-node').forEach(node => {
|
root.querySelectorAll('.rack-topology-node').forEach(node => {
|
||||||
const bounds = node.getBoundingClientRect();
|
const bounds = node.getBoundingClientRect();
|
||||||
const x = bounds.left - rootBounds.left + root.scrollLeft;
|
const x = bounds.left - rootBounds.left + root.scrollLeft;
|
||||||
@@ -417,20 +545,20 @@
|
|||||||
const color = getComputedStyle(node).getPropertyValue('--node-color').trim() || '#1685fc';
|
const color = getComputedStyle(node).getPropertyValue('--node-color').trim() || '#1685fc';
|
||||||
const cellId = `node-${drawioId++}`;
|
const cellId = `node-${drawioId++}`;
|
||||||
nodeCells.set(node.dataset.nodeId, cellId);
|
nodeCells.set(node.dataset.nodeId, cellId);
|
||||||
drawioCells += `<mxCell id="${cellId}" value="${xmlEscape(node.querySelector('strong').textContent)}<br><font style="font-size:9px">${xmlEscape(node.querySelector('small').textContent)}</font>" style="rounded=1;html=1;whiteSpace=wrap;align=left;verticalAlign=middle;spacingLeft=8;fillColor=#162e45;strokeColor=${xmlEscape(color)};strokeWidth=2;fontColor=#ffffff;fontSize=12;" vertex="1" parent="1"><mxGeometry x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" as="geometry"/></mxCell>`;
|
drawioCells += `<mxCell id="${cellId}" value="${xmlEscape(node.querySelector('strong').textContent)}<br><font style="font-size:9px">${xmlEscape(node.querySelector('small').textContent)}</font>" style="rounded=1;html=1;whiteSpace=wrap;align=left;verticalAlign=middle;spacingLeft=8;fillColor=${palette.deviceFill};strokeColor=${xmlEscape(color)};strokeWidth=2;fontColor=${palette.deviceText};fontSize=12;" vertex="1" parent="1"><mxGeometry x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" as="geometry"/></mxCell>`;
|
||||||
content += `<rect x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" rx="6" fill="#162e45" stroke="${xmlEscape(color)}" stroke-width="2"/>`;
|
content += `<rect x="${x}" y="${y}" width="${bounds.width}" height="${bounds.height}" rx="6" fill="${palette.deviceFill}" stroke="${xmlEscape(color)}" stroke-width="2"/>`;
|
||||||
content += `<text x="${x + 10}" y="${y + 20}" fill="#fff" font-family="sans-serif" font-size="12" font-weight="600">${xmlEscape(node.querySelector('strong').textContent)}</text>`;
|
content += `<text x="${x + 10}" y="${y + 20}" fill="${palette.deviceText}" font-family="sans-serif" font-size="12" font-weight="600">${xmlEscape(node.querySelector('strong').textContent)}</text>`;
|
||||||
content += `<text x="${x + 10}" y="${y + 37}" fill="#cad5df" font-family="sans-serif" font-size="9">${xmlEscape(node.querySelector('small').textContent)}</text>`;
|
content += `<text x="${x + 10}" y="${y + 37}" fill="${palette.deviceSubtext}" font-family="sans-serif" font-size="9">${xmlEscape(node.querySelector('small').textContent)}</text>`;
|
||||||
});
|
});
|
||||||
let edgeContent = '';
|
let edgeContent = '';
|
||||||
data.edges.forEach(edge => {
|
data.edges.forEach(edge => {
|
||||||
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"/>`;
|
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 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(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;" edge="1" parent="1" source="${sourceId}" target="${targetId}"><mxGeometry relative="1" as="geometry"/></mxCell>`;
|
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>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height};
|
return {svg: svgDocument(width, height, edgeContent + content), drawio: drawioDocument(drawioCells), width, height};
|
||||||
@@ -483,6 +611,7 @@
|
|||||||
}));
|
}));
|
||||||
requestAnimationFrame(drawEdges);
|
requestAnimationFrame(drawEdges);
|
||||||
window.addEventListener('resize', drawEdges);
|
window.addEventListener('resize', drawEdges);
|
||||||
|
root.addEventListener('scroll', drawEdges, {passive: true});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
|||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from extras.models import Tag, SavedFilter
|
from extras.models import Tag, SavedFilter
|
||||||
from wireless.models import WirelessLink
|
from wireless.models import WirelessLAN, WirelessLink
|
||||||
from netbox.views.generic import (
|
from netbox.views.generic import (
|
||||||
ObjectView,
|
ObjectView,
|
||||||
ObjectListView,
|
ObjectListView,
|
||||||
@@ -927,6 +927,13 @@ def _rack_endpoint(termination):
|
|||||||
"rack_name": getattr(getattr(device, "rack", None), "name", None) or "Extern",
|
"rack_name": getattr(getattr(device, "rack", None), "name", None) or "Extern",
|
||||||
"url": device.get_absolute_url() if device else None,
|
"url": device.get_absolute_url() if device else None,
|
||||||
"color": getattr(getattr(device, "role", None), "color", None) or "1685fc",
|
"color": getattr(getattr(device, "role", None), "color", None) or "1685fc",
|
||||||
|
"position": float(device.position or 1) if device else None,
|
||||||
|
"height": float(max(getattr(getattr(device, "device_type", None), "u_height", 1) or 1, 1)) if device else 1,
|
||||||
|
"face": str(device.face or "front").lower() if device else "front",
|
||||||
|
"rack_u_height": getattr(getattr(device, "rack", None), "u_height", None),
|
||||||
|
"rack_desc_units": getattr(getattr(device, "rack", None), "desc_units", False),
|
||||||
|
"rack_starting_unit": getattr(getattr(device, "rack", None), "starting_unit", 1),
|
||||||
|
"subtitle": str(device.device_type) if device else getattr(termination, "name", str(termination)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -960,27 +967,42 @@ class RackElevationView(PermissionRequiredMixin, View):
|
|||||||
device_ids = []
|
device_ids = []
|
||||||
for device in devices.filter(rack_id__in=rack_ids).order_by("rack_id", "position", "name"):
|
for device in devices.filter(rack_id__in=rack_ids).order_by("rack_id", "position", "name"):
|
||||||
device_ids.append(device.pk)
|
device_ids.append(device.pk)
|
||||||
height = max(getattr(device.device_type, "u_height", 1) or 1, 1)
|
height = float(max(getattr(device.device_type, "u_height", 1) or 1, 1))
|
||||||
rack_height = max(device.rack.u_height or 1, 1)
|
rack_height = max(device.rack.u_height or 1, 1)
|
||||||
device.elevation_bottom = f"{(float(device.position or 1) - 1) / rack_height * 100:.4f}"
|
rack_start = float(device.rack.starting_unit or 1)
|
||||||
|
position_offset = float(device.position or rack_start) - rack_start
|
||||||
|
if device.rack.desc_units:
|
||||||
|
bottom_units = rack_height - position_offset - height
|
||||||
|
else:
|
||||||
|
bottom_units = position_offset
|
||||||
|
device.elevation_bottom = f"{max(bottom_units, 0) / rack_height * 100:.4f}"
|
||||||
device.elevation_height = f"{min(height / rack_height * 100, 100):.4f}"
|
device.elevation_height = f"{min(height / rack_height * 100, 100):.4f}"
|
||||||
device.elevation_face = str(device.face or "front").lower()
|
device.elevation_face = str(device.face or "front").lower()
|
||||||
rack_devices[device.rack_id].append(device)
|
rack_devices[device.rack_id].append(device)
|
||||||
|
|
||||||
for rack in rack_list:
|
for rack in rack_list:
|
||||||
rack.elevation_devices = rack_devices[rack.pk]
|
rack.elevation_devices = rack_devices[rack.pk]
|
||||||
rack.elevation_unit_labels = range(rack.u_height or 0, 0, -1)
|
rack_start = rack.starting_unit or 1
|
||||||
|
if rack.desc_units:
|
||||||
|
rack.elevation_unit_labels = range(rack_start, rack_start + (rack.u_height or 0))
|
||||||
|
else:
|
||||||
|
rack.elevation_unit_labels = range(
|
||||||
|
rack_start + (rack.u_height or 0) - 1,
|
||||||
|
rack_start - 1,
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
rack_export = []
|
rack_export = []
|
||||||
for rack in rack_list:
|
for rack in rack_list:
|
||||||
rack_export.append({
|
rack_export.append({
|
||||||
"id": rack.pk, "name": rack.name, "u_height": rack.u_height,
|
"id": rack.pk, "name": rack.name, "u_height": rack.u_height,
|
||||||
|
"starting_unit": rack.starting_unit, "desc_units": rack.desc_units,
|
||||||
"site": str(rack.site), "location": str(rack.location or ""),
|
"site": str(rack.site), "location": str(rack.location or ""),
|
||||||
"devices": [{
|
"devices": [{
|
||||||
"name": device.name or str(device.device_type),
|
"name": device.name or str(device.device_type),
|
||||||
"type": str(device.device_type),
|
"type": str(device.device_type),
|
||||||
"position": float(device.position or 1),
|
"position": float(device.position or 1),
|
||||||
"height": max(device.device_type.u_height or 1, 1),
|
"height": float(max(device.device_type.u_height or 1, 1)),
|
||||||
"face": device.elevation_face,
|
"face": device.elevation_face,
|
||||||
"color": f"#{device.role.color}" if device.role.color else "#1685fc",
|
"color": f"#{device.role.color}" if device.role.color else "#1685fc",
|
||||||
} for device in rack.elevation_devices],
|
} for device in rack.elevation_devices],
|
||||||
@@ -1012,6 +1034,56 @@ class RackElevationView(PermissionRequiredMixin, View):
|
|||||||
"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 "#1685fc",
|
||||||
"url": cable.get_absolute_url(),
|
"url": cable.get_absolute_url(),
|
||||||
|
"kind": "cable",
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
).select_related(
|
||||||
|
"interface_a__device__rack", "interface_a__device__role", "interface_a__device__device_type",
|
||||||
|
"interface_b__device__rack", "interface_b__device__role", "interface_b__device__device_type",
|
||||||
|
)
|
||||||
|
for wireless_link in wireless_links:
|
||||||
|
a = _rack_endpoint(wireless_link.interface_a)
|
||||||
|
b = _rack_endpoint(wireless_link.interface_b)
|
||||||
|
topology_nodes[a["node_id"]] = a
|
||||||
|
topology_nodes[b["node_id"]] = b
|
||||||
|
topology_edges.append({
|
||||||
|
"from": a["node_id"], "to": b["node_id"],
|
||||||
|
"from_device": a["device_name"], "to_device": b["device_name"],
|
||||||
|
"from_port": a["name"], "to_port": b["name"],
|
||||||
|
"label": wireless_link.ssid or str(wireless_link),
|
||||||
|
"color": "#8b5cf6", "url": wireless_link.get_absolute_url(),
|
||||||
|
"kind": "wireless",
|
||||||
|
})
|
||||||
|
|
||||||
|
wireless_lans = WirelessLAN.objects.restrict(request.user, "view").filter(
|
||||||
|
interfaces__device_id__in=device_ids
|
||||||
|
).distinct()
|
||||||
|
for wireless_lan in wireless_lans:
|
||||||
|
wlan_node_id = f"wlan-{wireless_lan.pk}"
|
||||||
|
topology_nodes[wlan_node_id] = {
|
||||||
|
"node_id": wlan_node_id, "device_id": None,
|
||||||
|
"device_name": f"WLAN · {wireless_lan.ssid}",
|
||||||
|
"name": wireless_lan.ssid, "subtitle": "Multi-Access WLAN",
|
||||||
|
"rack_id": None, "rack_name": "Funknetze",
|
||||||
|
"url": wireless_lan.get_absolute_url(), "color": "8b5cf6",
|
||||||
|
"position": None, "height": 1, "face": "front", "rack_u_height": None,
|
||||||
|
}
|
||||||
|
wlan_interfaces = Interface.objects.restrict(request.user, "view").filter(
|
||||||
|
wireless_lans=wireless_lan
|
||||||
|
).select_related("device__rack", "device__role", "device__device_type")
|
||||||
|
for interface in wlan_interfaces:
|
||||||
|
endpoint = _rack_endpoint(interface)
|
||||||
|
topology_nodes[endpoint["node_id"]] = endpoint
|
||||||
|
topology_edges.append({
|
||||||
|
"from": endpoint["node_id"], "to": wlan_node_id,
|
||||||
|
"from_device": endpoint["device_name"],
|
||||||
|
"to_device": f"WLAN · {wireless_lan.ssid}",
|
||||||
|
"from_port": endpoint["name"], "to_port": wireless_lan.ssid,
|
||||||
|
"label": wireless_lan.ssid, "color": "#8b5cf6",
|
||||||
|
"url": wireless_lan.get_absolute_url(), "kind": "wireless-lan",
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, "netbox_topology_views/rack_elevation.html", {
|
return render(request, "netbox_topology_views/rack_elevation.html", {
|
||||||
|
|||||||
Reference in New Issue
Block a user