Compare commits

..
20 Commits
Author SHA1 Message Date
mattieserver 79a234fcc9 v4.3.0 (#642) 2025-05-28 16:19:42 +02:00
larsen0815 db76752a82 Clearly specify file locations (#638) 2025-05-15 11:47:29 +02:00
Mario 47f263f5c2 Update version table in README.md (#637) 2025-05-15 09:21:27 +02:00
larsen0815 6eb20004f9 Improve installation instructions (#635) 2025-05-15 09:16:37 +02:00
Mario 836d9f40d7 Fix group boxes too small for multiline labels (#632) 2025-05-11 11:43:11 +02:00
Mario 607347ff18 Fix unnecessary field User id (#631) 2025-05-09 17:55:50 +02:00
Chris Russell 0f6051731a Fixes #627 - update template_content to models tuple (#628) 2025-05-05 12:59:03 +02:00
Kenneth Hardy 3c9aebfd2f Update README.md (#625)
Bumping version. Works on my machine.
2025-04-30 08:02:01 +02:00
Maurice Kok 60a5fe1eae fix for issue 593 (#626) 2025-04-30 08:01:13 +02:00
mattieserver 3fbeb97810 bump version (#619) 2025-03-19 22:26:39 +01:00
Tobias Genannt f3b822e18d Use new version identifier from Netbox (#608) 2025-03-19 22:08:21 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 555c1fac4c Bump esbuild and esbuild-sass-plugin (#610)
Bumps [esbuild](https://github.com/evanw/esbuild) and [esbuild-sass-plugin](https://github.com/glromeo/esbuild-sass-plugin). These dependencies needed to be updated together.

Updates `esbuild` from 0.12.29 to 0.25.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2021.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.12.29...v0.25.0)

Updates `esbuild-sass-plugin` from 1.8.2 to 3.3.1
- [Release notes](https://github.com/glromeo/esbuild-sass-plugin/releases)
- [Commits](https://github.com/glromeo/esbuild-sass-plugin/commits/v3.3.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
- dependency-name: esbuild-sass-plugin
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-19 22:07:20 +01:00
mattieserver cf8641939a basic support for v4.2 (#592)
* basic support for v4.2

* bump versions

* update readme with versions

* upgrade version for v4.1.X
2025-01-10 15:41:44 +01:00
Mario 2893fff111 Closes #484: Show device information (#573)
* Add model and individual option

* Add forms and views

* minor changes
2024-11-22 21:27:25 +01:00
Mario 25fdcb6cf4 fix saved filter not honoring show_unconnected (#582) 2024-10-18 14:58:33 +02:00
Chris Russell 5d330bc830 Update plugin yaml for topology views v4.0.1 and netbox v4.1.4 (#581) 2024-10-17 17:11:23 +01:00
Nat Morris 03b8c9c76e ENG-3602 initial plugin manifest (#578)
* chore: trigger a central workflow when the manifest is modified.
* chore: import compatibility from the existing registry details
2024-10-14 18:07:58 +01:00
Mario 02a7118a33 Fixes #547: XML Export does not work in modal window (#572)
* Fix XML export in modal windows

* Removed useless concatenation

* Revert removed basePath in JS and added basePath to HTMX template
2024-09-27 21:44:06 +02:00
Mario 44e49495fe Add API functions for coordinates and coordinate groups (#571) 2024-09-27 20:46:58 +02:00
mattieserver f4b1e34826 fix name of devices (#567)
* fix name of devices

* upgrade version
2024-09-11 17:00:15 +02:00
25 changed files with 1511 additions and 519 deletions
+15
View File
@@ -0,0 +1,15 @@
name: NetBox plugin manifest modified
on:
push:
branches: [ develop ]
paths:
- netbox-plugin.yaml
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
manifest-modified:
uses: netboxlabs/public-workflows/.github/workflows/reusable-plugin-manifest-modified.yml@release
+36 -31
View File
@@ -14,31 +14,15 @@ Options to export to xml (for draw.io/diagrams.net) or png.
## Install
**_NOTE:_** For docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
**_NOTE:_** For Docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
**_NOTE:_** Add `RUN mkdir -p /opt/netbox/netbox/static/netbox_topology_views/img` to the Dockerfile-Plugins file to create the image folder
**_NOTE:_** Add `RUN mkdir -p /opt/netbox/netbox/static/netbox_topology_views/img` to the Dockerfile-Plugins file to create the image folder.
The plugin is available as a Python package and can be installed with pip.
Run `pip install netbox-topology-views` in your virtual env.
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
```no-highlight
# echo netbox-topology-views >> local_requirements.txt
```
Once installed, the plugin needs to be enabled in your `configuration.py`
```python
# In your configuration.py
PLUGINS = ["netbox_topology_views"]
```
First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
Then run
Then run:
```bash
cd /opt/netbox/netbox
pip3 install netbox-topology-views
@@ -46,22 +30,43 @@ python3 manage.py migrate netbox_topology_views
python3 manage.py collectstatic --no-input
```
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
```no-highlight
echo netbox-topology-views >> /opt/netbox/local_requirements.txt
```
Once installed, the plugin needs to be enabled in your `configuration.py`:
```no-highlight
vim /opt/netbox/netbox/netbox/configuration.py
```
```python
PLUGINS = ["netbox_topology_views"]
```
Finally, restart NetBox:
```no-highlight
systemctl restart netbox
```
### Versions
> [!NOTE]
> If the version of netbox is not listed here the plugin will most likely not work. Remove the plugin if you want to update netbox before this plugin has released an update for that netbox version.
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| >= 4.0.0 | >= v4.0.0 |
| >= 3.7.0 | >= v3.9.0 |
| >= 3.6.4 | >= v3.8.1 |
| >= 3.6.0 | >= v3.7.X |
| >= 3.5.0 | >= v3.4.X |
| >= 3.4.0 | >= v3.X.X |
| >= 3.3.0 | >= v3.0.0 |
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
| >= 2.11.1 | >= v0.5.3 |
| >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 |
| = 4.3.X | = v4.3.0 |
| = 4.2.1 | = v4.2.1 |
| = 4.2.0 | = v4.2.0 |
| = 4.1.X | = v4.1.0 |
| = 4.0.X | = v4.0.0 |
| = 3.7.0 | = v3.9.0 |
| = 3.6.4 | = v3.8.1 |
| = 3.6.0 | = v3.7.0 |
| = 3.5.0 | = v3.6.2 |
### Update
+3 -3
View File
@@ -1,7 +1,7 @@
version: '3.4'
services:
postgres:
image: postgres:12-alpine
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
POSTGRES_DB: netbox
@@ -9,7 +9,7 @@ services:
ports:
- "5432:5432"
volumes:
- netbox-postgres-data:/var/lib/postgresql/data
- netbox-postgres-data-16:/var/lib/postgresql/data
redis:
image: redis:6-alpine
@@ -35,7 +35,7 @@ services:
- "6378:6379"
volumes:
netbox-postgres-data:
netbox-postgres-data-16:
driver: local
netbox-redis-data:
driver: local
+30
View File
@@ -0,0 +1,30 @@
version: 0.1
package_name: netbox-topology-views
compatibility:
- release: 4.3.0
netbox_min: 4.3.0
netbox_max: 4.3.1
- release: 4.2.0
netbox_min: 4.2.0
netbox_max: 4.2.1
- release: 4.1.0
netbox_min: 4.1.0
netbox_max: 4.1.11
- release: 4.0.1
netbox_min: 4.0.9
netbox_max: 4.0.11
- release: 4.0.0
netbox_min: 4.0.0
netbox_max: 4.0.9
- release: 3.9.1
netbox_min: 3.7.1
netbox_max: 3.7.8
- release: 3.9.0
netbox_min: 3.7.1
netbox_max: 3.7.8
- release: 3.8.1
netbox_min: 3.6.9
netbox_max: 3.6.9
- release: 3.6.2
netbox_min: 3.5.1
netbox_max: 3.5.9
+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.0.1"
version = "4.3.0"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
+1 -1
View File
@@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
fields = ("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", "group_sites", "group_locations", "group_racks", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size")
fields = ("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", "group_sites", "group_locations", "group_racks", "group_virtualchassis", "draw_default_layout", "straight_cables", "grid_size", "node_label_items")
+5
View File
@@ -7,5 +7,10 @@ router = NetBoxRouter()
router.register("save-coords", views.SaveCoordsViewSet)
router.register("images", views.SaveRoleImageViewSet)
router.register("xml-export", views.ExportTopoToXML, "xml-export")
router.register("coordinate-groups", views.CoordinateGroupViewSet)
router.register("coordinate", views.CoordinateViewSet)
router.register("circuitcoordinate", views.CircuitCoordinateViewSet)
router.register("powerpanelcoordinate", views.PowerPanelCoordinateViewSet)
router.register("powerfeedcoordinate", views.PowerFeedCoordinateViewSet)
urlpatterns = router.urls
+31 -3
View File
@@ -1,7 +1,7 @@
from typing import Dict
import sys
from netbox.api.viewsets import BaseViewSet
from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
@@ -17,6 +17,11 @@ from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from netbox_topology_views.api.serializers import (
RoleImageSerializer,
TopologyDummySerializer,
CoordinateGroupSerializer,
CoordinateSerializer,
CircuitCoordinateSerializer,
PowerPanelCoordinateSerializer,
PowerFeedCoordinateSerializer,
)
import netbox_topology_views.models
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
@@ -109,7 +114,7 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
if request.GET:
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 = get_query_settings(request)
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)
# Read options from saved filters as NetBox does not handle custom plugin filters
if "filter_id" in request.GET and request.GET["filter_id"] != '':
@@ -119,6 +124,7 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
if ignore_cable_type == () and 'ignore_cable_type' in saved_filter_params: ignore_cable_type = saved_filter_params['ignore_cable_type']
if save_coords == False and 'save_coords' in saved_filter_params: save_coords = saved_filter_params['save_coords']
if show_unconnected == False and 'show_unconnected' in saved_filter_params: show_unconnected = saved_filter_params['show_unconnected']
if show_power == False and 'show_power' in saved_filter_params: show_power = saved_filter_params['show_power']
if show_circuit == False and 'show_circuit' in saved_filter_params: show_circuit = saved_filter_params['show_circuit']
if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
@@ -132,6 +138,7 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
if straight_cables == False and 'straight_cables' in saved_filter_params: show_neighbors = saved_filter_params['straight_cables']
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size']
if node_label_items == () and 'node_label_items' in saved_filter_params: node_label_items = saved_filter_params['node_label_items']
except SavedFilter.DoesNotExist: # filter_id not found
pass
except Exception as inst:
@@ -164,8 +171,9 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
group_id=group_id,
straight_cables=straight_cables,
grid_size=grid_size,
node_label_items=node_label_items,
)
xml_data = export_data_to_xml(topo_data).decode('utf-8')
xml_data = export_data_to_xml(topo_data).decode('utf-8').replace('\n', '&#xa;')
return HttpResponse(xml_data, content_type="application/xml; charset=utf-8")
else:
@@ -251,3 +259,23 @@ class SaveRoleImageViewSet(BaseViewSet, ReadOnlyModelViewSet):
)
return JsonResponse({"status": "Ok"})
class CoordinateGroupViewSet(NetBoxModelViewSet):
queryset = CoordinateGroup.objects.all()
serializer_class = CoordinateGroupSerializer
class CoordinateViewSet(NetBoxModelViewSet):
queryset = Coordinate.objects.all()
serializer_class = CoordinateSerializer
class CircuitCoordinateViewSet(NetBoxModelViewSet):
queryset = CircuitCoordinate.objects.all()
serializer_class = CircuitCoordinateSerializer
class PowerPanelCoordinateViewSet(NetBoxModelViewSet):
queryset = PowerPanelCoordinate.objects.all()
serializer_class = PowerPanelCoordinateSerializer
class PowerFeedCoordinateViewSet(NetBoxModelViewSet):
queryset = PowerFeedCoordinate.objects.all()
serializer_class = PowerFeedCoordinateSerializer
+14
View File
@@ -0,0 +1,14 @@
from utilities.choices import ChoiceSet
class NodeLabelItems(ChoiceSet):
DEVICE_NAME = 'devicename'
PRIMARY_IPV4 = 'primaryipv4'
PRIMARY_IPV6 = 'primaryipv6'
OUT_OF_BAND_IP = 'outofbandip'
CHOICES = [
(DEVICE_NAME, 'Device Name'),
(PRIMARY_IPV4, 'Primary IPv4'),
(PRIMARY_IPV6, 'Primary IPv6'),
(OUT_OF_BAND_IP, 'Out-of-band IP'),
]
+1 -1
View File
@@ -71,7 +71,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
null_value=None,
)
mac_address = MultiValueMACAddressFilter(
field_name='interfaces__mac_address',
field_name='interfaces__mac_addresses__mac_address',
label="MAC address",
)
serial = MultiValueCharFilter(
+15 -3
View File
@@ -23,6 +23,7 @@ from utilities.forms.fields import (
)
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
from netbox_topology_views.choices import NodeLabelItems
class DeviceFilterForm(
LocalConfigContextFilterForm,
@@ -36,7 +37,7 @@ class DeviceFilterForm(
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',
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options")
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', 'node_label_items', name=_("Options")
),
FieldSet('id', name=_("Device")),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
@@ -335,6 +336,11 @@ class DeviceFilterForm(
max_value=1000,
help_text=_('Show grid and snap dragged icons to grid. Set to 0 to disable grid and snapping.')
)
node_label_items = forms.MultipleChoiceField(
label=_('Node Label Items'),
required=False,
choices=NodeLabelItems
)
class CoordinateGroupsForm(NetBoxModelForm):
fieldsets = (
@@ -510,7 +516,6 @@ class IndividualOptionsForm(NetBoxModelForm):
fieldsets = (
FieldSet
(
'user_id',
'ignore_cable_type',
'preselected_device_roles',
'preselected_tags',
@@ -530,6 +535,7 @@ class IndividualOptionsForm(NetBoxModelForm):
'draw_default_layout',
'straight_cables',
'grid_size',
'node_label_items',
),
)
@@ -681,6 +687,12 @@ class IndividualOptionsForm(NetBoxModelForm):
'Integers between 0 and 1000 are allowed. Snap to grid will be '
'automatically enabled for values > 0.')
)
node_label_items = forms.MultipleChoiceField(
label=_('Node Label Items'),
required=False,
choices=NodeLabelItems,
help_text=_('Choose Label Items that you want to be displayed.')
)
class Meta:
model = IndividualOptions
@@ -689,5 +701,5 @@ class IndividualOptionsForm(NetBoxModelForm):
'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'
'straight_cables', 'grid_size', 'node_label_items'
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2024-09-27 18:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0010_individualoptions_grid_size'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='node_label_items',
field=models.CharField(blank=True, max_length=255),
),
]
+6 -1
View File
@@ -25,6 +25,7 @@ from netbox_topology_views.utils import (
image_static_url,
)
from netbox_topology_views.choices import NodeLabelItems
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
class Meta:
@@ -407,7 +408,11 @@ class IndividualOptions(NetBoxModel):
grid_size = models.PositiveSmallIntegerField(
default=0
)
node_label_items = models.CharField(
max_length=255,
blank=True
)
_netbox_private = True
def __str___(self):
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 l=(o,t)=>()=>(o&&(t=o(o=0)),t);var k=(o,t)=>()=>(t||o((t={exports:{}}).exports,t),t.exports);var d=(o,t,e)=>new Promise((n,r)=>{var c=s=>{try{i(e.next(s))}catch(a){r(a)}},y=s=>{try{i(e.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,y);i((e=e.apply(o,t)).next())});var g,m=l(()=>{g=o=>{if(!document.cookie)return;let t=null,e=document.cookie.split(";");for(let n=0;n<e.length;n++){let r=e[n].trim();if(r.substring(0,o.length+1)===o+"="){t=decodeURIComponent(r.substring(o.length+1));break}}return t}});var u,p=l(()=>{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 S=k(w=>{m();p();var f={},h=g("csrftoken");document.querySelector("form#images").addEventListener("submit",o=>d(null,null,function*(){o.preventDefault();try{let t=yield fetch("/"+basePath+"api/plugins/netbox_topology_views/images/save/",{method:"POST",body:JSON.stringify(f),headers:{"X-CSRFToken":h,"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:n}}=t.currentTarget;f[e]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${e}`);r&&(r.innerHTML=`<img src="${n}" />`)})})});S();})();
@@ -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": []
"mappings": "oTAAA,IAAaA,EAAbC,EAAAC,EAAA,KAAaF,EAAaG,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,IAAIC,EAAc,KACZC,EAAU,SAAS,OAAO,MAAM,GAAG,EAEzC,QAASC,EAAI,EAAGA,EAAID,EAAQ,OAAQC,IAAK,CACrC,IAAMC,EAASF,EAAQC,CAAC,EAAE,KAAK,EAE/B,GAAIC,EAAO,UAAU,EAAGJ,EAAK,OAAS,CAAC,IAAMA,EAAO,IAAK,CACrDC,EAAc,mBAAmBG,EAAO,UAAUJ,EAAK,OAAS,CAAC,CAAC,EAClE,KACJ,CACJ,CAEA,OAAOC,CACX,IChBA,IAAaI,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,IACAC,IAEA,IAAMC,EAAU,CAAC,EACXC,EAAYC,EAAU,WAAW,EAEvC,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,UAAUL,CAAO,EAC5B,QAAS,CACL,cAAeC,EACf,eAAgB,kBACpB,CACJ,CAAC,EAED,GAAI,CAACI,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,CA3BxC,IAAAM,EA4BQ,GAAI,EAAEN,EAAE,yBAAyB,aAAc,OAC/C,GAAM,CACF,QAAS,CAAE,KAAAO,EAAM,MAAAC,CAAM,CAC3B,EAAIR,EAAE,cAENH,EAAQU,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": ["getCookie", "init_csrftoken", "__esmMin", "name", "cookieValue", "cookies", "i", "cookie", "toast", "init_toast", "__esmMin", "message", "el", "content", "require_images", "__commonJSMin", "exports", "init_csrftoken", "init_toast", "mapping", "csrftoken", "getCookie", "e", "__async", "res", "toast", "err", "el", "_a", "role", "image", "button"]
}
+16 -8
View File
@@ -333,7 +333,7 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
// Extract node ids and node type ids from all nodes
for (let [key, value] of nodes._data) {
if (value[typeId] != undefined) {
nodesArray.push([value.id, value[typeId], value[type]]);
nodesArray.push([value.id, value[typeId], value[type], value.label_num_lines]);
}
}
// Split single array above into arrays grouped by node id
@@ -390,16 +390,24 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
const rectangles = [];
const xValues = [];
const yValues = [];
const yValuesWithLabel = [];
const labelLineHeight = 16;
for(let val of value[1]) {
// Get coordinates for every node in a given group
for(let val of value[1]) {
xValues.push(graph.getPosition(val[0]).x);
yValues.push(graph.getPosition(val[0]).y);
yValuesWithLabel.push(yValues[yValues.length -1] + val[3] * labelLineHeight);
}
const rectX = Math.min(...xValues) - rectParams.paddingX;
const rectY = Math.min(...yValues) - rectParams.paddingY;
const rectSizeX = Math.max(...xValues) - Math.min(...xValues) + 2*rectParams.paddingX;
const rectSizeY = Math.max(...yValues) - Math.min(...yValues) + 2*rectParams.paddingY;
const minX = Math.min(...xValues);
const maxX = Math.max(...xValues);
const minY = Math.min(...yValues);
const maxY = Math.max(...yValuesWithLabel);
const rectX = minX - rectParams.paddingX;
const rectY = minY - rectParams.paddingY;
const rectSizeX = maxX - minX + 2*rectParams.paddingX;
const rectSizeY = maxY - minY + 2*rectParams.paddingY;
rectangles.push({
ctx: canvascontext,
@@ -511,12 +519,12 @@ function performXmlDownload() {
if (curr_url.includes(sites_prefix)) {
var site_id = curr_url.split(sites_prefix)[1];
site_id = site_id.split('/')[0]
xml_search_options = 'site_id=' + site_id + '&show_cables=on&show_unconnected=on'
xml_search_options = 'site_id=' + site_id + '&show_cables=True&show_unconnected=True'
}
else if (curr_url.includes(location_prefix)) {
var location_id = curr_url.split(location_prefix)[1];
location_id = location_id.split('/')[0]
xml_search_options = 'location_id=' + location_id + '&show_cables=on&show_unconnected=on'
xml_search_options = 'location_id=' + location_id + '&show_cables=True&show_unconnected=True'
}
}
else {
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "4.0.1",
"version": "4.3.0",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
@@ -15,8 +15,8 @@
"devDependencies": {
"@egjs/hammerjs": "^2.0.0",
"component-emitter": "^1.3.0",
"esbuild": "^0.12.24",
"esbuild-sass-plugin": "^1.5.2",
"esbuild": "^0.25.0",
"esbuild-sass-plugin": "^3.3.1",
"keycharm": "^0.4.0",
"timsort": "^0.3.0",
"uuid": "^8.0.0"
+3 -3
View File
@@ -3,16 +3,16 @@ from netbox.plugins import PluginTemplateExtension
from django.conf import settings
from packaging import version
NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
NETBOX_CURRENT_VERSION = version.parse(settings.RELEASE.version)
class SiteButtons(PluginTemplateExtension):
model = 'dcim.site'
models = ('dcim.site', )
def buttons(self):
return self.render('netbox_topology_views/site_button.html')
class LocationButtons(PluginTemplateExtension):
model = 'dcim.location'
models = ('dcim.location', )
def buttons(self):
return self.render('netbox_topology_views/location_button.html')
@@ -40,5 +40,6 @@
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>
+5 -1
View File
@@ -200,7 +200,11 @@ def get_query_settings(request):
if "grid_size" in request.GET:
grid_size = request.GET.getlist('grid_size')
return 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 = ()
if "node_label_items" in request.GET:
node_label_items = request.GET.getlist('node_label_items')
return 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
class LinePattern():
wireless = [2, 10, 2, 10]
+43 -15
View File
@@ -78,7 +78,7 @@ from netbox_topology_views.utils import (
IMAGE_FILETYPES
)
from netbox_topology_views.choices import NodeLabelItems
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
is_device = isinstance(entity, Device)
@@ -97,7 +97,10 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
def create_node(
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, group_id="default"
device: Union[Device, Circuit, PowerPanel, PowerFeed],
save_coords: bool,
node_label_items: list,
group_id="default"
):
node = {}
node_content = ""
@@ -146,7 +149,7 @@ def create_node(
model_name = 'Coordinate'
dev_name = device.name
if dev_name is None:
dev_name = device.device_type.get_full_name
dev_name = device.device_type.full_name
if device.device_type is not None:
node_content += (
@@ -229,10 +232,26 @@ def create_node(
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
node["title"] = dev_title
node["name"] = dev_name
node["label"] = dev_name
# Create a list of possible label items. Omit None types
label_mapping = {
NodeLabelItems.DEVICE_NAME: dev_name,
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),
}
label_items = []
for item in node_label_items:
if label_mapping[item] is not None:
label_items.append(str(label_mapping[item]))
node_label = '\n'.join(label_items)
node["label"] = node_label
node["shape"] = "image"
node["href"] = device.get_absolute_url()
node["image"] = get_image_for_entity(device)
node["label_num_lines"] = len(label_items)
return node
@@ -350,6 +369,7 @@ def get_topology_data(
group_id,
straight_cables: bool,
grid_size: list,
node_label_items: list,
):
supported_termination_types = []
@@ -402,8 +422,8 @@ def get_topology_data(
if show_circuit:
circuit_terminations = CircuitTermination.objects.filter(
Q(site_id__in=site_ids) | Q(provider_network__isnull=False)
).prefetch_related("provider_network", "circuit")
Q(_site_id__in=site_ids) | Q(_provider_network__isnull=False)
)
for circuit_termination in circuit_terminations:
circuit_termination: CircuitTermination
if (
@@ -424,14 +444,14 @@ def get_topology_data(
termination_b = create_circuit_termination(
circuit_termination.cable.b_terminations[0]
)
elif circuit_termination.provider_network is not None:
elif circuit_termination.termination is not None:
if (
circuit_termination.provider_network_id
circuit_termination.termination_id
not in nodes_provider_networks
):
nodes_provider_networks[
circuit_termination.provider_network.pk
] = circuit_termination.provider_network
circuit_termination.termination.pk
] = circuit_termination.termination
if bool(termination_a) and bool(termination_b):
circuit_model = {
@@ -472,7 +492,7 @@ def get_topology_data(
] = circuit_termination.circuit
for d in nodes_circuits.values():
nodes.append(create_node(d, save_coords, group_id))
nodes.append(create_node(d, save_coords, node_label_items, group_id))
if show_power:
power_panels_ids = PowerPanel.objects.filter(
@@ -523,10 +543,10 @@ def get_topology_data(
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
for d in nodes_powerfeed.values():
nodes.append(create_node(d, save_coords, group_id))
nodes.append(create_node(d, save_coords, node_label_items ,group_id))
for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords, group_id))
nodes.append(create_node(d, save_coords, node_label_items, group_id))
if show_logical_connections:
interfaces = Interface.objects.filter(
@@ -698,7 +718,7 @@ def get_topology_data(
results = {}
for d in nodes_devices.values():
nodes.append(create_node(d, save_coords, group_id))
nodes.append(create_node(d, save_coords, node_label_items, group_id))
results["nodes"] = nodes
results["edges"] = edges
@@ -729,7 +749,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET:
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 = get_query_settings(request)
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)
# Read options from saved filters as NetBox does not handle custom plugin filters
if "filter_id" in request.GET and request.GET["filter_id"] != '':
@@ -739,6 +759,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if ignore_cable_type == () and 'ignore_cable_type' in saved_filter_params: ignore_cable_type = saved_filter_params['ignore_cable_type']
if save_coords == False and 'save_coords' in saved_filter_params: save_coords = saved_filter_params['save_coords']
if show_unconnected == False and 'show_unconnected' in saved_filter_params: show_unconnected = saved_filter_params['show_unconnected']
if show_power == False and 'show_power' in saved_filter_params: show_power = saved_filter_params['show_power']
if show_circuit == False and 'show_circuit' in saved_filter_params: show_circuit = saved_filter_params['show_circuit']
if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
@@ -752,6 +773,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
if straight_cables == False and 'straight_cables' in saved_filter_params: straight_cables = saved_filter_params['straight_cables']
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size']
if node_label_items == () and 'node_label_items' in saved_filter_params: node_label_items = saved_filter_params['node_label_items']
except SavedFilter.DoesNotExist: # filter_id not found
pass
except Exception as inst:
@@ -786,6 +808,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
group_id=group_id,
straight_cables=straight_cables,
grid_size=grid_size,
node_label_items=node_label_items,
)
else:
@@ -815,6 +838,10 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if individualOptions.group_virtualchassis: q['group_virtualchassis'] = "True"
if individualOptions.straight_cables: q['straight_cables'] = "True"
if individualOptions.grid_size: q['grid_size'] = individualOptions.grid_size
node_label_items = IndividualOptions.objects.get(id=individualOptions.id).node_label_items.translate({ord(i): None for i in '[]\''}).split(', ')
if node_label_items == ['']: node_label_items = []
q.setlist("node_label_items", node_label_items)
if individualOptions.draw_default_layout:
q['draw_init'] = "True"
else:
@@ -1176,6 +1203,7 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
'draw_default_layout': queryset.draw_default_layout,
'straight_cables': queryset.straight_cables,
'grid_size': queryset.grid_size,
'node_label_items': tuple(queryset.node_label_items.translate({ord(i): None for i in '[]\''}).split(', ')),
},
)
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup(
name="netbox-topology-views",
version="4.0.1",
version="4.3.0",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",