Compare commits

...
39 Commits
Author SHA1 Message Date
Mario 8aea42c50b v4.5.1 (#721) 2026-03-29 10:51:01 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> f56b8a4aec Bump picomatch from 2.3.1 to 2.3.2 in /netbox_topology_views/static_dev (#719)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 09:56:55 +02:00
Mario 3aa2bab141 Add option to show cable labels (#720) 2026-03-29 09:52:10 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6c4106e428 Bump immutable from 5.1.2 to 5.1.5 in /netbox_topology_views/static_dev (#710)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.2 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.2...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 11:07:12 +01:00
Mario 3d97f649d9 Add Asset Tag to node label items (#712) 2026-03-10 10:13:04 +01:00
Mario d10ced8157 Fix preselected tags not recognized 2026-03-10 10:07:04 +01:00
Mario 9d4ef31bdd Closes #701: Option to always show cable/interface labels on edges (#707)
* Added draw termination labels
2026-03-10 09:34:45 +01:00
Mario e20f250425 Revert last Show Wireless change (#703) 2026-01-27 19:24:19 +01:00
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
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
31 changed files with 1812 additions and 684 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
+38 -32
View File
@@ -14,31 +14,15 @@ Options to export to xml (for draw.io/diagrams.net) or png.
## Install ## 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. 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. First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
Then run:
Then run
```bash ```bash
cd /opt/netbox/netbox cd /opt/netbox/netbox
pip3 install netbox-topology-views pip3 install netbox-topology-views
@@ -46,23 +30,45 @@ python3 manage.py migrate netbox_topology_views
python3 manage.py collectstatic --no-input 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 ### 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 | | netbox version | netbox-topology-views version |
| -------------- | ----------------------------- | | -------------- | ----------------------------- |
| >= 4.1.0 | >= v4.1.0 | | = 4.5.X | = v4.5.1 |
| >= 4.0.0 | >= v4.0.0 | | = 4.4.X | = v4.4.0 |
| >= 3.7.0 | >= v3.9.0 | | = 4.3.X | = v4.3.0 |
| >= 3.6.4 | >= v3.8.1 | | = 4.2.1 | = v4.2.1 |
| >= 3.6.0 | >= v3.7.X | | = 4.2.0 | = v4.2.0 |
| >= 3.5.0 | >= v3.4.X | | = 4.1.X | = v4.1.0 |
| >= 3.4.0 | >= v3.X.X | | = 4.0.X | = v4.0.0 |
| >= 3.3.0 | >= v3.0.0 | | = 3.7.0 | = v3.9.0 |
| >= 3.2.0 | >= v1.1.0 | | = 3.6.4 | = v3.8.1 |
| >= 3.1.8 | >= v1.0.0 | | = 3.6.0 | = v3.7.0 |
| >= 2.11.1 | >= v0.5.3 | | = 3.5.0 | = v3.6.2 |
| >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 |
### Update ### Update
+3 -3
View File
@@ -1,7 +1,7 @@
version: '3.4' version: '3.4'
services: services:
postgres: postgres:
image: postgres:12-alpine image: postgres:16-alpine
environment: environment:
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
POSTGRES_DB: netbox POSTGRES_DB: netbox
@@ -9,7 +9,7 @@ services:
ports: ports:
- "5432:5432" - "5432:5432"
volumes: volumes:
- netbox-postgres-data:/var/lib/postgresql/data - netbox-postgres-data-16:/var/lib/postgresql/data
redis: redis:
image: redis:6-alpine image: redis:6-alpine
@@ -35,7 +35,7 @@ services:
- "6378:6379" - "6378:6379"
volumes: volumes:
netbox-postgres-data: netbox-postgres-data-16:
driver: local driver: local
netbox-redis-data: netbox-redis-data:
driver: local driver: local
+36
View File
@@ -0,0 +1,36 @@
version: 0.1
package_name: netbox-topology-views
compatibility:
- release: 4.5.0
netbox_min: 4.5.0
netbox_max: 4.5.5
- 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
- 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
+2 -2
View File
@@ -4,8 +4,8 @@ from netbox.plugins import PluginConfig
class TopologyViewsConfig(PluginConfig): class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views" name = "netbox_topology_views"
verbose_name = "Topology views" verbose_name = "Topology views"
description = "An plugin to render topology maps" description = "A plugin to render topology maps"
version = "4.1.0" version = "4.5.1"
author = "Mattijs Vanhaverbeke" author = "Mattijs Vanhaverbeke"
author_email = "author@example.com" author_email = "author@example.com"
base_url = "netbox_topology_views" base_url = "netbox_topology_views"
+4 -1
View File
@@ -50,4 +50,7 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
class IndividualOptionsSerializer(NetBoxModelSerializer): class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = IndividualOptions 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",
"draw_termination_labels", "draw_cable_labels", "grid_size", "node_label_items")
+5
View File
@@ -7,5 +7,10 @@ router = NetBoxRouter()
router.register("save-coords", views.SaveCoordsViewSet) router.register("save-coords", views.SaveCoordsViewSet)
router.register("images", views.SaveRoleImageViewSet) router.register("images", views.SaveRoleImageViewSet)
router.register("xml-export", views.ExportTopoToXML, "xml-export") 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 urlpatterns = router.urls
+36 -4
View File
@@ -1,7 +1,7 @@
from typing import Dict from typing import Dict
import sys import sys
from netbox.api.viewsets import BaseViewSet from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet
from circuits.models import Circuit from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel 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 ( from netbox_topology_views.api.serializers import (
RoleImageSerializer, RoleImageSerializer,
TopologyDummySerializer, TopologyDummySerializer,
CoordinateGroupSerializer,
CoordinateSerializer,
CircuitCoordinateSerializer,
PowerPanelCoordinateSerializer,
PowerFeedCoordinateSerializer,
) )
import netbox_topology_views.models import netbox_topology_views.models
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
@@ -109,7 +114,7 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
if request.GET: 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, draw_termination_labels, draw_cable_labels, grid_size, node_label_items = get_query_settings(request)
# Read options from saved filters as NetBox does not handle custom plugin filters # Read options from saved filters as NetBox does not handle custom plugin filters
if "filter_id" in request.GET and request.GET["filter_id"] != '': 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 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 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_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_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'] if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
@@ -130,8 +136,11 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks'] if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks']
if group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis'] if group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis']
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] 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 straight_cables == False and 'straight_cables' in saved_filter_params: straight_cables = saved_filter_params['straight_cables']
if draw_termination_labels == False and 'draw_termination_labels' in saved_filter_params: draw_termination_labels = saved_filter_params['draw_termination_labels']
if draw_cable_labels == False and 'draw_cable_labels' in saved_filter_params: draw_cable_labels = saved_filter_params['draw_cable_labels']
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size'] 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 except SavedFilter.DoesNotExist: # filter_id not found
pass pass
except Exception as inst: except Exception as inst:
@@ -163,9 +172,12 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
group_virtualchassis=group_virtualchassis, group_virtualchassis=group_virtualchassis,
group_id=group_id, group_id=group_id,
straight_cables=straight_cables, straight_cables=straight_cables,
draw_termination_labels=draw_termination_labels,
draw_cable_labels=draw_cable_labels,
grid_size=grid_size, 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") return HttpResponse(xml_data, content_type="application/xml; charset=utf-8")
else: else:
@@ -251,3 +263,23 @@ class SaveRoleImageViewSet(BaseViewSet, ReadOnlyModelViewSet):
) )
return JsonResponse({"status": "Ok"}) 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
+36
View File
@@ -0,0 +1,36 @@
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'
ASSET_TAG = 'assettag'
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'),
(ASSET_TAG, 'Asset Tag'),
]
+1 -1
View File
@@ -71,7 +71,7 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
null_value=None, null_value=None,
) )
mac_address = MultiValueMACAddressFilter( mac_address = MultiValueMACAddressFilter(
field_name='interfaces__mac_address', field_name='interfaces__mac_addresses__mac_address',
label="MAC address", label="MAC address",
) )
serial = MultiValueCharFilter( serial = MultiValueCharFilter(
+105 -52
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.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
from netbox_topology_views.choices import NodeLabelItems
class DeviceFilterForm( class DeviceFilterForm(
LocalConfigContextFilterForm, LocalConfigContextFilterForm,
@@ -34,9 +35,13 @@ class DeviceFilterForm(
fieldsets = ( fieldsets = (
FieldSet('q', 'filter_id', 'tag'), FieldSet('q', 'filter_id', 'tag'),
FieldSet( FieldSet(
'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_wireless',
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors',
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options") 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables',
'draw_termination_labels', 'draw_cable_labels', 'grid_size', 'node_label_items', name=_("Options")
),
FieldSet(
'show_circuit', 'show_power', name=_("Additional filter-independent types (non-devices)")
), ),
FieldSet('id', name=_("Device")), FieldSet('id', name=_("Device")),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
@@ -260,7 +265,12 @@ class DeviceFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES 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
)
)
show_logical_connections = forms.NullBooleanField( show_logical_connections = forms.NullBooleanField(
label =_('Show Logical Connections'), required=False, initial=False, label =_('Show Logical Connections'), required=False, initial=False,
widget=forms.Select( widget=forms.Select(
@@ -279,24 +289,6 @@ class DeviceFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES 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( group_sites = forms.NullBooleanField(
label =_('Group Sites'), required=False, initial=False, label =_('Group Sites'), required=False, initial=False,
widget=forms.Select( widget=forms.Select(
@@ -327,6 +319,18 @@ class DeviceFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )
) )
draw_termination_labels = forms.BooleanField(
label=_('Termination Labels'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
draw_cable_labels = forms.BooleanField(
label=_('Cable Labels'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
grid_size = forms.IntegerField( grid_size = forms.IntegerField(
label=_('Grid Size'), label=_('Grid Size'),
required=False, required=False,
@@ -335,6 +339,27 @@ class DeviceFilterForm(
max_value=1000, max_value=1000,
help_text=_('Show grid and snap dragged icons to grid. Set to 0 to disable grid and snapping.') 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
)
# 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
)
)
class CoordinateGroupsForm(NetBoxModelForm): class CoordinateGroupsForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
@@ -510,26 +535,32 @@ class IndividualOptionsForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
FieldSet FieldSet
( (
'user_id',
'ignore_cable_type', 'ignore_cable_type',
'preselected_device_roles', 'preselected_device_roles',
'preselected_tags', 'preselected_tags',
'save_coords', 'save_coords',
'show_unconnected', 'show_unconnected',
'show_cables', 'show_cables',
'show_wireless',
'show_logical_connections', 'show_logical_connections',
'show_single_cable_logical_conns', 'show_single_cable_logical_conns',
'show_neighbors', 'show_neighbors',
'show_circuit',
'show_power',
'show_wireless',
'group_sites', 'group_sites',
'group_locations', 'group_locations',
'group_racks', 'group_racks',
'group_virtualchassis', 'group_virtualchassis',
'draw_default_layout', 'draw_default_layout',
'straight_cables', 'straight_cables',
'draw_termination_labels',
'draw_cable_labels',
'grid_size', 'grid_size',
'node_label_items',
),
FieldSet
(
'show_circuit',
'show_power',
name=_("Additional filter-independent types (non-devices)")
), ),
) )
@@ -582,6 +613,13 @@ class IndividualOptionsForm(NetBoxModelForm):
'with one or more cables. These connections are displayed as solid ' 'with one or more cables. These connections are displayed as solid '
'lines in the color of the cable.') 'lines in the color of the cable.')
) )
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.')
)
show_logical_connections = forms.BooleanField( show_logical_connections = forms.BooleanField(
label =_('Show Logical Connections'), label =_('Show Logical Connections'),
required=False, required=False,
@@ -607,28 +645,6 @@ class IndividualOptionsForm(NetBoxModelForm):
'Link peers will be added if \'Show Cables\' is ticked, far-end ' 'Link peers will be added if \'Show Cables\' is ticked, far-end '
'terminations will be added if \'Show Logical Connections\' is ticked.') '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( group_sites = forms.BooleanField(
label =_('Group Sites'), label =_('Group Sites'),
required=False, required=False,
@@ -671,6 +687,19 @@ class IndividualOptionsForm(NetBoxModelForm):
help_text=_('Enable this option if you want to draw cables as straight lines ' help_text=_('Enable this option if you want to draw cables as straight lines '
'instead of curves.') 'instead of curves.')
) )
draw_termination_labels = forms.BooleanField(
label=('Termination Labels'),
required=False,
initial=False,
help_text=_('Enable this option if you want to draw a label on each '
'cable end.')
)
draw_cable_labels = forms.BooleanField(
label=('Cable Labels'),
required=False,
initial=False,
help_text=_('Enable this option if you want to draw a cable labels.')
)
grid_size = forms.IntegerField( grid_size = forms.IntegerField(
label=_('Grid Size'), label=_('Grid Size'),
required=True, required=True,
@@ -681,13 +710,37 @@ class IndividualOptionsForm(NetBoxModelForm):
'Integers between 0 and 1000 are allowed. Snap to grid will be ' 'Integers between 0 and 1000 are allowed. Snap to grid will be '
'automatically enabled for values > 0.') '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.')
)
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.')
)
class Meta: class Meta:
model = IndividualOptions model = IndividualOptions
fields = [ fields = [
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags',
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'save_coords', 'show_unconnected', 'show_cables', 'show_wireless',
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors',
'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'draw_default_layout', 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis',
'straight_cables', 'grid_size' 'draw_default_layout', 'straight_cables', 'draw_termination_labels', 'draw_cable_labels',
'grid_size', 'node_label_items', 'show_circuit', 'show_power'
] ]
@@ -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),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2026-02-19 18:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0011_individualoptions_node_label_items'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='draw_termination_labels',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.12 on 2026-03-26 07:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0012_individualoptions_draw_termination_labels'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='draw_cable_labels',
field=models.BooleanField(default=False),
),
]
+12 -1
View File
@@ -25,6 +25,7 @@ from netbox_topology_views.utils import (
image_static_url, image_static_url,
) )
from netbox_topology_views.choices import NodeLabelItems
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin): class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
class Meta: class Meta:
@@ -404,10 +405,20 @@ class IndividualOptions(NetBoxModel):
straight_cables = models.BooleanField( straight_cables = models.BooleanField(
default=False default=False
) )
draw_termination_labels = models.BooleanField(
default=False
)
draw_cable_labels = models.BooleanField(
default=False
)
grid_size = models.PositiveSmallIntegerField( grid_size = models.PositiveSmallIntegerField(
default=0 default=0
) )
node_label_items = models.CharField(
max_length=255,
blank=True
)
_netbox_private = True _netbox_private = True
def __str___(self): 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 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, "version": 3,
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"], "sources": ["../../../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", "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": [] "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
}
+70 -14
View File
@@ -1,6 +1,43 @@
import { DataSet } from 'vis-data/esnext' import { DataSet } from 'vis-data/esnext'
import { Network } from 'vis-network/esnext' import { Network } from 'vis-network/esnext'
import { getCookie } from './csrftoken.js'
function generatePortSVG(text) {
// pre-calculate text width
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
const fontFamily = 'helvetica';
const fontSize = 14;
textEl.textContent = text;
textEl.setAttribute('font-family', fontFamily);
textEl.setAttribute('font-size', fontSize + 'px');
svg.appendChild(textEl);
document.body.appendChild(svg);
const textWidth = textEl.getComputedTextLength();
textEl.remove();
svg.remove();
const rectPadding = 2;
const rectBorderWidth = 2;
const rectX = rectBorderWidth / 2;
const rectY = rectBorderWidth / 2;
const rectWidth = textWidth + rectBorderWidth + rectPadding * 2;
const rectHeight = fontSize + rectBorderWidth + rectPadding * 2;
const svgHeight = rectHeight + rectY + rectBorderWidth / 2;
const svgWidth = rectWidth + rectX + rectBorderWidth / 2;
const textX = svgWidth / 2 ;
const textY = svgHeight / 2;
// generate SVG
return 'data:image/svg+xml;charset=utf-8,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="' + (svgWidth) + '" height="' + (svgHeight) + '">' +
'<rect x="' + rectX + '" y="' + rectY + '" rx="4" ry="4" width="' + (rectWidth) + '" height="' + (rectHeight) + '" fill="#ffdf3f" stroke="#ffc93f" stroke-width="' + (rectBorderWidth) + '" style="opacity:0.5"/>' +
'<text x="' + textX + '" y="' + textY + '" font-family="' + fontFamily + '" font-size="' + (fontSize) + '" fill="black" text-anchor="middle" dominant-baseline="middle">' + text + '</text>' +
'</svg>'
)
}
const options = { const options = {
interaction: { interaction: {
@@ -36,9 +73,6 @@ const options = {
} }
} }
// Load CSRF token
const csrftoken = getCookie('csrftoken')
// Render vis graph // Render vis graph
let graph = null // vis graph instance let graph = null // vis graph instance
@@ -66,7 +100,21 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
const edges = new DataSet( const edges = new DataSet(
topologyData.edges.map((node) => ({ topologyData.edges.map((node) => ({
...node, ...node,
title: htmlTitle(node.title) title: htmlTitle(node.title),
...(node.drawTerminationLabel && {
arrows: {
from: {
enabled: true,
type: "image",
src: generatePortSVG(node.cable_a_name)
},
to: {
enabled: true,
type: "image",
src: generatePortSVG(node.cable_b_name)
}
}
})
})) }))
) )
@@ -228,7 +276,7 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
{ {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'X-CSRFToken': csrftoken, 'X-CSRFToken': window.CSRF_TOKEN,
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
@@ -333,7 +381,7 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
// Extract node ids and node type ids from all nodes // Extract node ids and node type ids from all nodes
for (let [key, value] of nodes._data) { for (let [key, value] of nodes._data) {
if (value[typeId] != undefined) { 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 // Split single array above into arrays grouped by node id
@@ -390,16 +438,24 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
const rectangles = []; const rectangles = [];
const xValues = []; const xValues = [];
const yValues = []; 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); xValues.push(graph.getPosition(val[0]).x);
yValues.push(graph.getPosition(val[0]).y); yValues.push(graph.getPosition(val[0]).y);
yValuesWithLabel.push(yValues[yValues.length -1] + val[3] * labelLineHeight);
} }
const rectX = Math.min(...xValues) - rectParams.paddingX; const minX = Math.min(...xValues);
const rectY = Math.min(...yValues) - rectParams.paddingY; const maxX = Math.max(...xValues);
const rectSizeX = Math.max(...xValues) - Math.min(...xValues) + 2*rectParams.paddingX; const minY = Math.min(...yValues);
const rectSizeY = Math.max(...yValues) - Math.min(...yValues) + 2*rectParams.paddingY; 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({ rectangles.push({
ctx: canvascontext, ctx: canvascontext,
@@ -511,12 +567,12 @@ function performXmlDownload() {
if (curr_url.includes(sites_prefix)) { if (curr_url.includes(sites_prefix)) {
var site_id = curr_url.split(sites_prefix)[1]; var site_id = curr_url.split(sites_prefix)[1];
site_id = site_id.split('/')[0] 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)) { else if (curr_url.includes(location_prefix)) {
var location_id = curr_url.split(location_prefix)[1]; var location_id = curr_url.split(location_prefix)[1];
location_id = location_id.split('/')[0] 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 { else {
@@ -1,8 +1,6 @@
import { getCookie } from './csrftoken.js'
import { toast } from './toast.js' import { toast } from './toast.js'
const mapping = {} const mapping = {}
const csrftoken = getCookie('csrftoken')
document.querySelector('form#images').addEventListener('submit', async (e) => { document.querySelector('form#images').addEventListener('submit', async (e) => {
e.preventDefault() e.preventDefault()
@@ -11,7 +9,7 @@ document.querySelector('form#images').addEventListener('submit', async (e) => {
method: 'POST', method: 'POST',
body: JSON.stringify(mapping), body: JSON.stringify(mapping),
headers: { headers: {
'X-CSRFToken': csrftoken, 'X-CSRFToken': window.CSRF_TOKEN,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}) })
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "4.1.0", "version": "4.5.1",
"scripts": { "scripts": {
"bundle": "node bundle.js", "bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles", "bundle:styles": "node bundle.js --styles",
@@ -15,8 +15,8 @@
"devDependencies": { "devDependencies": {
"@egjs/hammerjs": "^2.0.0", "@egjs/hammerjs": "^2.0.0",
"component-emitter": "^1.3.0", "component-emitter": "^1.3.0",
"esbuild": "^0.12.24", "esbuild": "^0.25.0",
"esbuild-sass-plugin": "^1.5.2", "esbuild-sass-plugin": "^3.3.1",
"keycharm": "^0.4.0", "keycharm": "^0.4.0",
"timsort": "^0.3.0", "timsort": "^0.3.0",
"uuid": "^8.0.0" "uuid": "^8.0.0"
-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.VERSION)
class SiteButtons(PluginTemplateExtension):
model = 'dcim.site'
def buttons(self):
return self.render('netbox_topology_views/site_button.html')
class LocationButtons(PluginTemplateExtension):
model = 'dcim.location'
def buttons(self):
return self.render('netbox_topology_views/location_button.html')
template_extensions = [SiteButtons, LocationButtons]
@@ -1,44 +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 }};
</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="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
<div class="panel-body"> <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 id="visgraph"></div>
</div> </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>
+15 -1
View File
@@ -196,11 +196,25 @@ def get_query_settings(request):
if request.GET["straight_cables"] == "True": if request.GET["straight_cables"] == "True":
straight_cables = True straight_cables = True
draw_termination_labels = False
if "draw_termination_labels" in request.GET:
if request.GET["draw_termination_labels"] == "True":
draw_termination_labels = True
draw_cable_labels = False
if "draw_cable_labels" in request.GET:
if request.GET["draw_cable_labels"] == "True":
draw_cable_labels = True
grid_size = 0 grid_size = 0
if "grid_size" in request.GET: if "grid_size" in request.GET:
grid_size = request.GET.getlist('grid_size') 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, draw_termination_labels, draw_cable_labels, grid_size, node_label_items
class LinePattern(): class LinePattern():
wireless = [2, 10, 2, 10] wireless = [2, 10, 2, 10]
+101 -35
View File
@@ -1,7 +1,6 @@
import json import json
from functools import reduce from functools import reduce
from typing import DefaultDict, Dict, Optional, Union from typing import DefaultDict, Dict, Optional, Union
import time
from itertools import chain from itertools import chain
from circuits.models import Circuit, CircuitTermination, ProviderNetwork 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 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 WirelessLink
from utilities.htmx import htmx_partial
from netbox.views.generic import ( from netbox.views.generic import (
ObjectView, ObjectView,
ObjectListView, ObjectListView,
@@ -78,7 +76,7 @@ from netbox_topology_views.utils import (
IMAGE_FILETYPES IMAGE_FILETYPES
) )
from netbox_topology_views.choices import NodeLabelItems
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
is_device = isinstance(entity, Device) is_device = isinstance(entity, Device)
@@ -97,7 +95,10 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
def create_node( 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 = {}
node_content = "" node_content = ""
@@ -229,10 +230,37 @@ def create_node(
dev_title = "<table><tbody> %s</tbody></table>" % (node_content) dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
node["title"] = dev_title node["title"] = dev_title
node["name"] = dev_name 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.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),
NodeLabelItems.ASSET_TAG: getattr(device, 'asset_tag', None),
}
label_items = []
for item in node_label_items:
if label_mapping[item] and 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["shape"] = "image"
node["href"] = device.get_absolute_url() node["href"] = device.get_absolute_url()
node["image"] = get_image_for_entity(device) node["image"] = get_image_for_entity(device)
node["label_num_lines"] = len(label_items)
return node return node
@@ -242,6 +270,8 @@ def create_edge(
termination_a: Dict, termination_a: Dict,
termination_b: Dict, termination_b: Dict,
straight_cables: bool, straight_cables: bool,
draw_termination_labels: bool,
draw_cable_labels: bool,
circuit: Optional[Dict] = None, circuit: Optional[Dict] = None,
cable: Optional[Cable] = None, cable: Optional[Cable] = None,
wireless: Optional[Dict] = None, wireless: Optional[Dict] = None,
@@ -294,10 +324,17 @@ def create_edge(
edge["dashes"] = LinePattern().logical edge["dashes"] = LinePattern().logical
edge["color"] = '#f1c232' edge["color"] = '#f1c232'
edge["href"] = interface.get_absolute_url() + "trace" 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
if draw_cable_labels is True:
edge["label"] = cable.label
else:
cable_label = ""
edge[ edge[
"title" "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: if cable is not None:
edge["href"] = cable.get_absolute_url() edge["href"] = cable.get_absolute_url()
@@ -307,6 +344,12 @@ def create_edge(
# if straight_cables == True: edge["smooth"] = False # if straight_cables == True: edge["smooth"] = False
edge["smooth"] = not straight_cables edge["smooth"] = not straight_cables
if draw_termination_labels is True:
edge["drawTerminationLabel"] = True
edge["cable_a_name"] = cable_a_name
edge["cable_b_name"] = cable_b_name
return edge return edge
@@ -349,7 +392,10 @@ def get_topology_data(
group_virtualchassis: bool, group_virtualchassis: bool,
group_id, group_id,
straight_cables: bool, straight_cables: bool,
draw_termination_labels: bool,
draw_cable_labels: bool,
grid_size: list, grid_size: list,
node_label_items: list,
): ):
supported_termination_types = [] supported_termination_types = []
@@ -402,8 +448,8 @@ def get_topology_data(
if show_circuit: if show_circuit:
circuit_terminations = CircuitTermination.objects.filter( circuit_terminations = CircuitTermination.objects.filter(
Q(site_id__in=site_ids) | Q(provider_network__isnull=False) Q(_site_id__in=site_ids) | Q(_provider_network__isnull=False)
).prefetch_related("provider_network", "circuit") )
for circuit_termination in circuit_terminations: for circuit_termination in circuit_terminations:
circuit_termination: CircuitTermination circuit_termination: CircuitTermination
if ( if (
@@ -417,21 +463,21 @@ def get_topology_data(
termination_a = {} termination_a = {}
termination_b = {} termination_b = {}
circuit_model = {} 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( termination_a = create_circuit_termination(
circuit_termination.cable.a_terminations[0] circuit_termination.cable.a_terminations[0]
) )
termination_b = create_circuit_termination( termination_b = create_circuit_termination(
circuit_termination.cable.b_terminations[0] circuit_termination.cable.b_terminations[0]
) )
elif circuit_termination.provider_network is not None: elif circuit_termination.termination is not None:
if ( if (
circuit_termination.provider_network_id circuit_termination.termination_id
not in nodes_provider_networks not in nodes_provider_networks
): ):
nodes_provider_networks[ nodes_provider_networks[
circuit_termination.provider_network.pk circuit_termination.termination.pk
] = circuit_termination.provider_network ] = circuit_termination.termination
if bool(termination_a) and bool(termination_b): if bool(termination_a) and bool(termination_b):
circuit_model = { circuit_model = {
@@ -446,6 +492,8 @@ def get_topology_data(
termination_a=termination_a, termination_a=termination_a,
termination_b=termination_b, termination_b=termination_b,
straight_cables=straight_cables, straight_cables=straight_cables,
draw_termination_labels=draw_termination_labels,
draw_cable_labels=draw_cable_labels,
) )
) )
@@ -472,7 +520,7 @@ def get_topology_data(
] = circuit_termination.circuit ] = circuit_termination.circuit
for d in nodes_circuits.values(): 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: if show_power:
power_panels_ids = PowerPanel.objects.filter( power_panels_ids = PowerPanel.objects.filter(
@@ -516,6 +564,8 @@ def get_topology_data(
termination_b=termination_b, termination_b=termination_b,
power=True, power=True,
straight_cables=straight_cables, straight_cables=straight_cables,
draw_termination_labels=draw_termination_labels,
draw_cable_labels=draw_cable_labels,
) )
) )
@@ -523,10 +573,10 @@ def get_topology_data(
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
for d in nodes_powerfeed.values(): 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(): 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: if show_logical_connections:
interfaces = Interface.objects.filter( interfaces = Interface.objects.filter(
@@ -554,7 +604,7 @@ def get_topology_data(
edge_ids += 1 edge_ids += 1
termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id } termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id }
termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id } termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id }
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=interface, straight_cables=straight_cables)) edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=interface, straight_cables=straight_cables, draw_termination_labels=draw_termination_labels, draw_cable_labels=draw_cable_labels))
nodes_devices[interface.device.id] = interface.device nodes_devices[interface.device.id] = interface.device
nodes_devices[destination.device.id] = destination.device nodes_devices[destination.device.id] = destination.device
@@ -635,6 +685,8 @@ def get_topology_data(
termination_a=termination_a, termination_a=termination_a,
termination_b=termination_b, termination_b=termination_b,
straight_cables=straight_cables, straight_cables=straight_cables,
draw_termination_labels=draw_termination_labels,
draw_cable_labels=draw_cable_labels,
) )
) )
@@ -675,6 +727,8 @@ def get_topology_data(
termination_b=termination_b, termination_b=termination_b,
wireless=wireless, wireless=wireless,
straight_cables=straight_cables, straight_cables=straight_cables,
draw_termination_labels=draw_termination_labels,
draw_cable_labels=draw_cable_labels,
) )
) )
@@ -698,7 +752,7 @@ def get_topology_data(
results = {} results = {}
for d in nodes_devices.values(): 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["nodes"] = nodes
results["edges"] = edges results["edges"] = edges
@@ -729,7 +783,10 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET: 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, draw_termination_labels, draw_cable_labels, 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 # Read options from saved filters as NetBox does not handle custom plugin filters
if "filter_id" in request.GET and request.GET["filter_id"] != '': if "filter_id" in request.GET and request.GET["filter_id"] != '':
@@ -739,6 +796,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 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 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_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_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'] if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
@@ -751,7 +809,10 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis'] if group_virtualchassis == False and 'group_virtualchassis' in saved_filter_params: group_virtualchassis = saved_filter_params['group_virtualchassis']
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] 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 straight_cables == False and 'straight_cables' in saved_filter_params: straight_cables = saved_filter_params['straight_cables']
if draw_termination_labels == False and 'draw_termination_labels' in saved_filter_params: draw_termination_labels = saved_filter_params['draw_termination_labels']
if draw_cable_labels == False and 'draw_cable_labels' in saved_filter_params: draw_cable_labels = saved_filter_params['draw_cable_labels']
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size'] 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 except SavedFilter.DoesNotExist: # filter_id not found
pass pass
except Exception as inst: except Exception as inst:
@@ -766,6 +827,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
group_id = request.GET["group"] 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": 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( topo_data = get_topology_data(
queryset=self.queryset, queryset=self.queryset,
individualOptions=individualOptions, individualOptions=individualOptions,
@@ -785,13 +848,19 @@ class TopologyHomeView(PermissionRequiredMixin, View):
group_virtualchassis=group_virtualchassis, group_virtualchassis=group_virtualchassis,
group_id=group_id, group_id=group_id,
straight_cables=straight_cables, straight_cables=straight_cables,
draw_termination_labels=draw_termination_labels,
draw_cable_labels=draw_cable_labels,
grid_size=grid_size, grid_size=grid_size,
node_label_items=node_label_items,
) )
if topo_data is None or not topo_data["nodes"]:
empty_result = True
else: else:
# No GET-Request in URL. We most likely came here from the navigation menu. # No GET-Request in URL. We most likely came here from the navigation menu.
preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True) preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True)
preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True) preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list('slug', flat=True)
ignore_cable_type = IndividualOptions.objects.get(id=individualOptions.id).ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ') ignore_cable_type = IndividualOptions.objects.get(id=individualOptions.id).ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')
if ignore_cable_type == ['']: ignore_cable_type = [] if ignore_cable_type == ['']: ignore_cable_type = []
@@ -814,7 +883,13 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if individualOptions.group_racks: q['group_racks'] = "True" if individualOptions.group_racks: q['group_racks'] = "True"
if individualOptions.group_virtualchassis: q['group_virtualchassis'] = "True" if individualOptions.group_virtualchassis: q['group_virtualchassis'] = "True"
if individualOptions.straight_cables: q['straight_cables'] = "True" if individualOptions.straight_cables: q['straight_cables'] = "True"
if individualOptions.draw_termination_labels: q['draw_termination_labels'] = "True"
if individualOptions.draw_cable_labels: q['draw_cable_labels'] = "True"
if individualOptions.grid_size: q['grid_size'] = individualOptions.grid_size 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: if individualOptions.draw_default_layout:
q['draw_init'] = "True" q['draw_init'] = "True"
else: else:
@@ -823,20 +898,6 @@ class TopologyHomeView(PermissionRequiredMixin, View):
query_string = q.urlencode() query_string = q.urlencode()
return HttpResponseRedirect(f"{request.path}?{query_string}") 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( return render(
request, request,
"netbox_topology_views/index.html", "netbox_topology_views/index.html",
@@ -846,6 +907,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"broken_image": find_image_url("role-unknown"), "broken_image": find_image_url("role-unknown"),
"model": self.model, "model": self.model,
"basepath": settings.BASE_PATH, "basepath": settings.BASE_PATH,
"filter_required": filter_required,
"empty_result": empty_result,
}, },
) )
@@ -1175,7 +1238,10 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
'group_virtualchassis': queryset.group_virtualchassis, 'group_virtualchassis': queryset.group_virtualchassis,
'draw_default_layout': queryset.draw_default_layout, 'draw_default_layout': queryset.draw_default_layout,
'straight_cables': queryset.straight_cables, 'straight_cables': queryset.straight_cables,
'draw_termination_labels': queryset.draw_termination_labels,
'draw_cable_labels': queryset.draw_cable_labels,
'grid_size': queryset.grid_size, 'grid_size': queryset.grid_size,
'node_label_items': tuple(queryset.node_label_items.translate({ord(i): None for i in '[]\''}).split(', ')),
}, },
) )
+2 -2
View File
@@ -7,8 +7,8 @@ long_description = readme.read_text()
setup( setup(
name="netbox-topology-views", name="netbox-topology-views",
version="4.1.0", version="4.5.1",
description="An NetBox plugin to create Topology maps", description="A NetBox plugin to create Topology maps",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://github.com/mattieserver/netbox-topology-views", url="https://github.com/mattieserver/netbox-topology-views",