Compare commits

...
13 Commits
Author SHA1 Message Date
Mario 89c56c8d1c v4.5.0 2026-01-07 11:08:58 +01:00
Go RayandMario 619e959a8e Fixes #683: Fix crash when rendering WirelessLink without label (#690)
* Fix crash when rendering WirelessLink without label

* removes the unnecessary str()

* Update views.py

---------

Co-authored-by: Mario <github@franzbonenkamp.de>
2025-12-30 22:31:14 +01:00
Mario 10f545b509 Reorganize non-device filters (#694) 2025-12-30 17:20:14 +01:00
Mario 3810e9567f Fix error for zero results (#692) 2025-12-30 15:00:01 +01:00
Mario 5981a21830 bump version (#682) 2025-10-15 17:34:20 +02:00
Mario 8100b8146f Remove modal window (#680) 2025-10-09 14:59:35 +02:00
Mario 6c047b1022 Add cable label in hover popup (#679) 2025-10-08 18:51:29 +02:00
valtersg 4afa4f721f Fixes #671 (#677)
* Use CSRF token from window.CSRF_TOKEN

* Removed unused csrftoken.js
2025-10-08 15:26:31 +02:00
Mario e7df2f6772 Update README.md (#670)
Added NetBox v4.4.x to version matrix.
2025-09-25 12:43:32 +02:00
Mario b85e548049 added empty result warning (#658) 2025-07-18 23:09:26 +02:00
Mario 097fe5c685 added some label items (#656) 2025-07-17 15:07:37 +02:00
尤理衡 (Li-Heng Yu) c83479fb1d Added check for cable integrity (#647) 2025-07-14 08:49:32 +02:00
mattieserver 79a234fcc9 v4.3.0 (#642) 2025-05-28 16:19:42 +02:00
20 changed files with 507 additions and 585 deletions
+3 -1
View File
@@ -57,7 +57,9 @@ systemctl restart netbox
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| = 4.3.X | = v4.2.1 |
| = 4.5.X | = v4.5.0 |
| = 4.4.X | = v4.4.0 |
| = 4.3.X | = v4.3.0 |
| = 4.2.1 | = v4.2.1 |
| = 4.2.0 | = v4.2.0 |
| = 4.1.X | = v4.1.0 |
+9
View File
@@ -1,6 +1,15 @@
version: 0.1
package_name: netbox-topology-views
compatibility:
- release: 4.5.0
netbox_min: 4.5.0
netbox_max: 4.5.1
- release: 4.4.0
netbox_min: 4.4.0
netbox_max: 4.4.3
- release: 4.3.0
netbox_min: 4.3.0
netbox_max: 4.3.7
- release: 4.2.0
netbox_min: 4.2.0
netbox_max: 4.2.1
+1 -1
View File
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "4.2.1"
version = "4.5.0"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
+20
View File
@@ -2,13 +2,33 @@ from utilities.choices import ChoiceSet
class NodeLabelItems(ChoiceSet):
DEVICE_NAME = 'devicename'
DEVICE_TYPE = 'devicetype'
ROLE = 'role'
DESCRIPTION = 'description'
PRIMARY_IPV4 = 'primaryipv4'
PRIMARY_IPV6 = 'primaryipv6'
OUT_OF_BAND_IP = 'outofbandip'
PLATFORM = 'platform'
SERIAL = 'serial'
TENANT = 'tenant'
SITE = 'site'
LOCATION = 'location'
RACK = 'rack'
VIRTUAL_CHASSIS = 'virtualchassis'
CHOICES = [
(DEVICE_NAME, 'Device Name'),
(DEVICE_TYPE, 'Device Type'),
(ROLE, 'Role'),
(DESCRIPTION, 'Description'),
(PRIMARY_IPV4, 'Primary IPv4'),
(PRIMARY_IPV6, 'Primary IPv6'),
(OUT_OF_BAND_IP, 'Out-of-band IP'),
(PLATFORM, 'Platform'),
(SERIAL, 'Serial'),
(TENANT, 'Tenant'),
(SITE, 'Site'),
(LOCATION, 'Location'),
(RACK, 'Rack'),
(VIRTUAL_CHASSIS, 'Virtual Chassis'),
]
+63 -47
View File
@@ -36,9 +36,12 @@ class DeviceFilterForm(
FieldSet('q', 'filter_id', 'tag'),
FieldSet(
'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
'show_single_cable_logical_conns', 'show_neighbors',
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', 'node_label_items', name=_("Options")
),
FieldSet(
'show_circuit', 'show_power', 'show_wireless', name=_("Additional filter-independent types (non-devices)")
),
FieldSet('id', name=_("Device")),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_("Operation")),
@@ -280,24 +283,6 @@ class DeviceFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_circuit = forms.NullBooleanField(
label=_('Show Circuit Terminations'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_power = forms.NullBooleanField(
label=_('Show Power Feeds'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_wireless = forms.NullBooleanField(
label =_('Show Wireless Links'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
group_sites = forms.NullBooleanField(
label =_('Group Sites'), required=False, initial=False,
widget=forms.Select(
@@ -342,6 +327,29 @@ class DeviceFilterForm(
choices=NodeLabelItems
)
# Additional types (non-devices)
show_circuit = forms.NullBooleanField(
label=_('Show Circuit Terminations'), required=False, initial=False,
help_text=_('All Circuits will be displayed. Circuits cannot be filtered.'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_power = forms.NullBooleanField(
label=_('Show Power Feeds'), required=False, initial=False,
help_text=_('All Power Feeds will be displayed. Power Feeds cannot be filtered.'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_wireless = forms.NullBooleanField(
label =_('Show Wireless Links'), required=False, initial=False,
help_text=_('All Wireless Links will be displayed. Wireless Links cannot be filtered.'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
class CoordinateGroupsForm(NetBoxModelForm):
fieldsets = (
FieldSet('name', 'description', name=_("Group Details")),
@@ -525,9 +533,6 @@ class IndividualOptionsForm(NetBoxModelForm):
'show_logical_connections',
'show_single_cable_logical_conns',
'show_neighbors',
'show_circuit',
'show_power',
'show_wireless',
'group_sites',
'group_locations',
'group_racks',
@@ -537,6 +542,13 @@ class IndividualOptionsForm(NetBoxModelForm):
'grid_size',
'node_label_items',
),
FieldSet
(
'show_circuit',
'show_power',
'show_wireless',
name=_("Additional filter-independent types (non-devices)")
),
)
user_id = forms.CharField(widget=forms.HiddenInput())
@@ -613,28 +625,6 @@ class IndividualOptionsForm(NetBoxModelForm):
'Link peers will be added if \'Show Cables\' is ticked, far-end '
'terminations will be added if \'Show Logical Connections\' is ticked.')
)
show_circuit = forms.BooleanField(
label=_('Show Circuit Terminations'),
required=False,
initial=False,
help_text=_('Displays connections between circuit terminations. '
'These connections are displayed as blue dashed lines.')
)
show_power = forms.BooleanField(
label=_('Show Power Feeds'),
required=False,
initial=False,
help_text=_('Displays connections between power outlets and power '
'ports. These connections are displayed as solid lines in the '
'color of the cable. This option depends on \'Show Cables\'.')
)
show_wireless = forms.BooleanField(
label =_('Show Wireless Links'),
required=False,
initial=False,
help_text=_('Displays wireless connections. These connections are '
'displayed as blue dotted lines.')
)
group_sites = forms.BooleanField(
label =_('Group Sites'),
required=False,
@@ -693,13 +683,39 @@ class IndividualOptionsForm(NetBoxModelForm):
choices=NodeLabelItems,
help_text=_('Choose Label Items that you want to be displayed.')
)
show_circuit = forms.BooleanField(
label=_('Show Circuit Terminations'),
required=False,
initial=False,
help_text=_('Displays connections between circuit terminations. '
'These connections are displayed as blue dashed lines. '
'Be aware that Circuits cannot be filtered.')
)
show_power = forms.BooleanField(
label=_('Show Power Feeds'),
required=False,
initial=False,
help_text=_('Displays connections between power outlets and power '
'ports. These connections are displayed as solid lines in the '
'color of the cable. This option depends on \'Show Cables\'. '
'Be aware that Power Feeds cannot be filtered.')
)
show_wireless = forms.BooleanField(
label =_('Show Wireless Links'),
required=False,
initial=False,
help_text=_('Displays wireless connections. These connections are '
'displayed as blue dotted lines. '
'Be aware that Wireless Links cannot be filtered.')
)
class Meta:
model = IndividualOptions
fields = [
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags',
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power',
'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'draw_default_layout',
'straight_cables', 'grid_size', 'node_label_items'
'show_single_cable_logical_conns', 'show_neighbors',
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis',
'draw_default_layout', 'straight_cables', 'grid_size', 'node_label_items',
'show_circuit', 'show_power', 'show_wireless'
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
(()=>{var l=(e,t,o)=>new Promise((n,r)=>{var c=s=>{try{i(o.next(s))}catch(a){r(a)}},m=s=>{try{i(o.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,m);i((o=o.apply(e,t)).next())});var d=e=>{if(!document.cookie)return;let t=null,o=document.cookie.split(";");for(let n=0;n<o.length;n++){let r=o[n].trim();if(r.substring(0,e.length+1)===e+"="){t=decodeURIComponent(r.substring(e.length+1));break}}return t};var u={success:e=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()},error:e=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()}};var g={},p=d("csrftoken");document.querySelector("form#images").addEventListener("submit",e=>l(void 0,null,function*(){e.preventDefault();try{let t=yield fetch("/"+basePath+"api/plugins/netbox_topology_views/images/save/",{method:"POST",body:JSON.stringify(g),headers:{"X-CSRFToken":p,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(e=>{e.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:o,image:n}}=t.currentTarget;g[o]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${o}`);r&&(r.innerHTML=`<img src="${n}" />`)})});})();
(()=>{var w=(o,t)=>()=>(o&&(t=o(o=0)),t);var f=(o,t)=>()=>(t||o((t={exports:{}}).exports,t),t.exports);var l=(o,t,e)=>new Promise((r,s)=>{var c=n=>{try{a(e.next(n))}catch(i){s(i)}},g=n=>{try{a(e.throw(n))}catch(i){s(i)}},a=n=>n.done?r(n.value):Promise.resolve(n.value).then(c,g);a((e=e.apply(o,t)).next())});var u,d=w(()=>{u={success:o=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let e=t.querySelector("span");e.textContent=o,new window.Toast(t).show()},error:o=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let e=t.querySelector("span");e.textContent=o,new window.Toast(t).show()}}});var y=f(p=>{d();var m={};document.querySelector("form#images").addEventListener("submit",o=>l(null,null,function*(){o.preventDefault();try{let t=yield fetch("/"+basePath+"api/plugins/netbox_topology_views/images/save/",{method:"POST",body:JSON.stringify(m),headers:{"X-CSRFToken":window.CSRF_TOKEN,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(o=>{o.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:e,image:r}}=t.currentTarget;m[e]=r;let s=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${e}`);s&&(s.innerHTML=`<img src="${r}" />`)})})});y();})();
@@ -1,6 +1,6 @@
{
"version": 3,
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
"mappings": "mNAAO,GAAM,GAAY,AAAC,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,GAAI,GAAc,KACZ,EAAU,SAAS,OAAO,MAAM,KAEtC,OAAS,GAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,GAAM,GAAS,EAAQ,GAAG,OAE1B,GAAI,EAAO,UAAU,EAAG,EAAK,OAAS,KAAO,EAAO,IAAK,CACrD,EAAc,mBAAmB,EAAO,UAAU,EAAK,OAAS,IAChE,OAIR,MAAO,ICfJ,GAAM,GAAQ,CACjB,QAAS,AAAC,GAAY,CAClB,GAAM,GAAK,SAAS,cAAc,kCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,QAEV,MAAO,AAAC,GAAY,CAChB,GAAM,GAAK,SAAS,cAAc,gCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,SCZd,GAAM,GAAU,GACV,EAAY,EAAU,aAE5B,SAAS,cAAc,eAAe,iBAAiB,SAAU,AAAO,GAAM,0BAC1E,EAAE,iBACF,GAAI,CACA,GAAM,GAAM,KAAM,OAAM,IAAM,SAAW,iDAAkD,CACvF,OAAQ,OACR,KAAM,KAAK,UAAU,GACrB,QAAS,CACL,cAAe,EACf,eAAgB,sBAIxB,GAAI,CAAC,EAAI,GAAI,KAAM,IAAI,OAAM,KAAM,GAAI,QACvC,EAAM,QAAQ,wBACT,EAAP,CACE,QAAQ,IAAI,GACZ,EAAM,MAAM,EAAI,aAIxB,SAAS,iBAAiB,kCAAkC,QAAQ,AAAC,GAAO,CACxE,EAAG,iBAAiB,QAAS,AAAC,GAAM,CA3BxC,MA4BQ,GAAI,CAAE,GAAE,wBAAyB,cAAc,OAC/C,GAAM,CACF,QAAS,CAAE,OAAM,UACjB,EAAE,cAEN,EAAQ,GAAQ,EAEhB,GAAM,GAAS,KAAE,cACZ,QAAQ,eADE,cAET,cAAc,sBAAsB,KAC1C,AAAI,GAAQ,GAAO,UAAY,aAAa",
"names": []
"sources": ["../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
"mappings": "oTAAA,IAAaA,EAAbC,EAAAC,EAAA,KAAaF,EAAQ,CACjB,QAAUG,GAAY,CAClB,IAAMC,EAAK,SAAS,cAAc,gCAAgC,EAClE,GAAI,CAACA,EAAI,OAAO,QAAQ,MAAM,iCAAiC,EAC/D,IAAMC,EAAUD,EAAG,cAAc,MAAM,EACvCC,EAAQ,YAAcF,EACR,IAAI,OAAO,MAAMC,CAAE,EAC3B,KAAK,CACf,EACA,MAAQD,GAAY,CAChB,IAAMC,EAAK,SAAS,cAAc,8BAA8B,EAChE,GAAI,CAACA,EAAI,OAAO,QAAQ,MAAM,iCAAiC,EAC/D,IAAMC,EAAUD,EAAG,cAAc,MAAM,EACvCC,EAAQ,YAAcF,EACR,IAAI,OAAO,MAAMC,CAAE,EAC3B,KAAK,CACf,CACJ,ICjBA,IAAAE,EAAAC,EAAAC,GAAA,CAAAC,IAEA,IAAMC,EAAU,CAAC,EAEjB,SAAS,cAAc,aAAa,EAAE,iBAAiB,SAAiBC,GAAMC,EAAA,sBAC1ED,EAAE,eAAe,EACjB,GAAI,CACA,IAAME,EAAM,MAAM,MAAM,IAAM,SAAW,iDAAkD,CACvF,OAAQ,OACR,KAAM,KAAK,UAAUH,CAAO,EAC5B,QAAS,CACL,cAAe,OAAO,WACtB,eAAgB,kBACpB,CACJ,CAAC,EAED,GAAI,CAACG,EAAI,GAAI,MAAM,IAAI,MAAM,MAAMA,EAAI,KAAK,CAAC,EAC7CC,EAAM,QAAQ,gBAAgB,CAClC,OAASC,EAAK,CACV,QAAQ,IAAIA,CAAG,EACfD,EAAM,MAAMC,EAAI,OAAO,CAC3B,CACJ,EAAC,EAED,SAAS,iBAAiB,gCAAgC,EAAE,QAASC,GAAO,CACxEA,EAAG,iBAAiB,QAAUL,GAAM,CAzBxC,IAAAM,EA0BQ,GAAI,EAAEN,EAAE,yBAAyB,aAAc,OAC/C,GAAM,CACF,QAAS,CAAE,KAAAO,EAAM,MAAAC,CAAM,CAC3B,EAAIR,EAAE,cAEND,EAAQQ,CAAI,EAAIC,EAEhB,IAAMC,GAASH,EAAAN,EAAE,cACZ,QAAQ,WAAW,IADT,YAAAM,EAET,cAAc,sBAAsBC,CAAI,IAC1CE,IAAQA,EAAO,UAAY,aAAaD,CAAK,OACrD,CAAC,CACL,CAAC",
"names": ["toast", "init_toast", "__esmMin", "message", "el", "content", "require_images", "__commonJSMin", "exports", "init_toast", "mapping", "e", "__async", "res", "toast", "err", "el", "_a", "role", "image", "button"]
}
@@ -1,17 +0,0 @@
export const getCookie = (name) => {
if (!document.cookie) return
let cookieValue = null
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
break
}
}
return cookieValue
}
+1 -5
View File
@@ -1,6 +1,5 @@
import { DataSet } from 'vis-data/esnext'
import { Network } from 'vis-network/esnext'
import { getCookie } from './csrftoken.js'
const options = {
interaction: {
@@ -36,9 +35,6 @@ const options = {
}
}
// Load CSRF token
const csrftoken = getCookie('csrftoken')
// Render vis graph
let graph = null // vis graph instance
@@ -228,7 +224,7 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
{
method: 'PATCH',
headers: {
'X-CSRFToken': csrftoken,
'X-CSRFToken': window.CSRF_TOKEN,
Accept: 'application/json',
'Content-Type': 'application/json'
},
@@ -1,8 +1,6 @@
import { getCookie } from './csrftoken.js'
import { toast } from './toast.js'
const mapping = {}
const csrftoken = getCookie('csrftoken')
document.querySelector('form#images').addEventListener('submit', async (e) => {
e.preventDefault()
@@ -11,7 +9,7 @@ document.querySelector('form#images').addEventListener('submit', async (e) => {
method: 'POST',
body: JSON.stringify(mapping),
headers: {
'X-CSRFToken': csrftoken,
'X-CSRFToken': window.CSRF_TOKEN,
'Content-Type': 'application/json'
}
})
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "4.2.0",
"version": "4.5.0",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
-20
View File
@@ -1,20 +0,0 @@
from netbox.plugins import PluginTemplateExtension
from django.conf import settings
from packaging import version
NETBOX_CURRENT_VERSION = version.parse(settings.RELEASE.version)
class SiteButtons(PluginTemplateExtension):
models = ('dcim.site', )
def buttons(self):
return self.render('netbox_topology_views/site_button.html')
class LocationButtons(PluginTemplateExtension):
models = ('dcim.location', )
def buttons(self):
return self.render('netbox_topology_views/location_button.html')
template_extensions = [SiteButtons, LocationButtons]
@@ -1,45 +0,0 @@
{% load static %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
<div class="modal-header">
<h5 class="modal-title">Topology Views</h5>
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadXml" class="btn btn-info" href="#">
<i class="mdi mdi-download"></i>
Download XML
</a>
<a id="btnDownloadImage" class="btn btn-info" href="#">
<i class="mdi mdi-download"></i>
Download PNG
</a>
<a class="btn btn-danger" data-bs-dismiss="modal">
<i class="mdi mdi-close-thick"></i>
Close
</a>
</div>
</div>
</div>
<div class="panel-body" >
<div style="max-width: 900px; height: 500px;">
<div id="visgraph" style="width: 100%; height: 100%;">
</div>
</div>
</div>
<style type="text/css">
.modal-dialog {
max-width: 902px !important;
}
</style>
<script type="text/javascript">
var is_htmx = true;
var brokenImage = '{{ broken_image }}';
var topologyData = {{ topology_data | safe }};
const basePath = '{{ basepath }}';
</script>
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
@@ -61,6 +61,13 @@
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
<div class="panel-body">
{% if filter_required %}
<div class="bg-warning text-white p-2">Nothing to display! Set filters and hit search button or set "Draw Default Layout" in your individual options.</div>
{% endif %}
{% if empty_result %}
<div class="bg-warning text-white p-2">Empty result set! Please review your filter settings.</div>
<div class="bg-info text-white p-2">Hint: Some options may produce an empty result. Use at least "Show Unconnected/Cables/Logical Connections".</div>
{% endif %}
<div id="visgraph"></div>
</div>
</div>
@@ -1,4 +0,0 @@
<a role="button" class="btn btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}&show_cables=True&show_unconnected=True" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
Topology
</a>
+29 -20
View File
@@ -1,7 +1,6 @@
import json
from functools import reduce
from typing import DefaultDict, Dict, Optional, Union
import time
from itertools import chain
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
@@ -28,7 +27,6 @@ from django.shortcuts import render, get_object_or_404
from django.views.generic import View
from extras.models import Tag, SavedFilter
from wireless.models import WirelessLink
from utilities.htmx import htmx_partial
from netbox.views.generic import (
ObjectView,
ObjectListView,
@@ -236,14 +234,24 @@ def create_node(
# Create a list of possible label items. Omit None types
label_mapping = {
NodeLabelItems.DEVICE_NAME: dev_name,
NodeLabelItems.DEVICE_TYPE: getattr(device, 'device_type', None),
NodeLabelItems.ROLE: getattr(device, 'role', None),
NodeLabelItems.DESCRIPTION: getattr(device, 'description', None),
NodeLabelItems.PRIMARY_IPV4: getattr(device, 'primary_ip4', None),
NodeLabelItems.PRIMARY_IPV6: getattr(device, 'primary_ip6', None),
NodeLabelItems.OUT_OF_BAND_IP: getattr(device, 'oob_ip', None),
NodeLabelItems.PLATFORM: getattr(device, 'platform', None),
NodeLabelItems.SERIAL: getattr(device, 'serial', None),
NodeLabelItems.TENANT: getattr(device, 'tenant', None),
NodeLabelItems.SITE: getattr(device, 'site', None),
NodeLabelItems.LOCATION: getattr(device, 'location', None),
NodeLabelItems.RACK: getattr(device, 'rack', None),
NodeLabelItems.VIRTUAL_CHASSIS: getattr(device, 'virtual_chassis', None),
}
label_items = []
for item in node_label_items:
if label_mapping[item] is not None:
if label_mapping[item] and label_mapping[item] is not None:
label_items.append(str(label_mapping[item]))
node_label = '\n'.join(label_items)
@@ -313,10 +321,15 @@ def create_edge(
edge["dashes"] = LinePattern().logical
edge["color"] = '#f1c232'
edge["href"] = interface.get_absolute_url() + "trace"
if cable is not None and hasattr(cable, "label") and cable.label:
cable_label = "<br>Label: " + cable.label
else:
cable_label = ""
edge[
"title"
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}]"
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}] {cable_label}"
if cable is not None:
edge["href"] = cable.get_absolute_url()
@@ -437,7 +450,7 @@ def get_topology_data(
termination_a = {}
termination_b = {}
circuit_model = {}
if circuit_termination.cable is not None:
if circuit_termination.cable is not None and bool(circuit_termination.cable.a_terminations) and bool(circuit_termination.cable.b_terminations):
termination_a = create_circuit_termination(
circuit_termination.cable.a_terminations[0]
)
@@ -751,6 +764,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, grid_size, node_label_items = get_query_settings(request)
filter_required = True
empty_result = False
# Read options from saved filters as NetBox does not handle custom plugin filters
if "filter_id" in request.GET and request.GET["filter_id"] != '':
try:
@@ -788,6 +804,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
group_id = request.GET["group"]
if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true":
filter_required = False
topo_data = get_topology_data(
queryset=self.queryset,
individualOptions=individualOptions,
@@ -810,6 +828,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
grid_size=grid_size,
node_label_items=node_label_items,
)
if topo_data is None or not topo_data["nodes"]:
empty_result = True
else:
# No GET-Request in URL. We most likely came here from the navigation menu.
@@ -850,20 +871,6 @@ class TopologyHomeView(PermissionRequiredMixin, View):
query_string = q.urlencode()
return HttpResponseRedirect(f"{request.path}?{query_string}")
if htmx_partial(request):
return render(
request,
"netbox_topology_views/htmx_topology.html",
{
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data),
"broken_image": find_image_url("role-unknown"),
"epoch": int(time.time()),
"basepath": settings.BASE_PATH,
},
)
return render(
request,
"netbox_topology_views/index.html",
@@ -873,6 +880,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"broken_image": find_image_url("role-unknown"),
"model": self.model,
"basepath": settings.BASE_PATH,
"filter_required": filter_required,
"empty_result": empty_result,
},
)
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup(
name="netbox-topology-views",
version="4.2.1",
version="4.5.0",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",