Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36dd761eaa | ||
|
|
8aea42c50b | ||
|
|
f56b8a4aec | ||
|
|
3aa2bab141 | ||
|
|
6c4106e428 | ||
|
|
3d97f649d9 | ||
|
|
d10ced8157 | ||
|
|
9d4ef31bdd | ||
|
|
e20f250425 | ||
|
|
89c56c8d1c | ||
|
|
619e959a8e | ||
|
|
10f545b509 | ||
|
|
3810e9567f | ||
|
|
5981a21830 | ||
|
|
8100b8146f | ||
|
|
6c047b1022 | ||
|
|
4afa4f721f | ||
|
|
e7df2f6772 | ||
|
|
b85e548049 | ||
|
|
097fe5c685 | ||
|
|
c83479fb1d | ||
|
|
79a234fcc9 | ||
|
|
db76752a82 | ||
|
|
47f263f5c2 | ||
|
|
6eb20004f9 | ||
|
|
836d9f40d7 | ||
|
|
607347ff18 | ||
|
|
0f6051731a | ||
|
|
3c9aebfd2f | ||
|
|
60a5fe1eae | ||
|
|
3fbeb97810 | ||
|
|
f3b822e18d | ||
|
|
555c1fac4c | ||
|
|
cf8641939a | ||
|
|
2893fff111 | ||
|
|
25fdcb6cf4 | ||
|
|
5d330bc830 | ||
|
|
03b8c9c76e | ||
|
|
02a7118a33 | ||
|
|
44e49495fe | ||
|
|
f4b1e34826 |
@@ -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
|
||||
@@ -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,45 @@ 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.5.X | = v4.5.1 |
|
||||
| = 4.4.X | = v4.4.0 |
|
||||
| = 4.3.X | = v4.3.0 |
|
||||
| = 4.2.1 | = v4.2.1 |
|
||||
| = 4.2.0 | = v4.2.0 |
|
||||
| = 4.1.X | = v4.1.0 |
|
||||
| = 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -4,8 +4,8 @@ from netbox.plugins import PluginConfig
|
||||
class TopologyViewsConfig(PluginConfig):
|
||||
name = "netbox_topology_views"
|
||||
verbose_name = "Topology views"
|
||||
description = "An plugin to render topology maps"
|
||||
version = "4.0.1"
|
||||
description = "A plugin to render topology maps"
|
||||
version = "4.5.1"
|
||||
author = "Mattijs Vanhaverbeke"
|
||||
author_email = "author@example.com"
|
||||
base_url = "netbox_topology_views"
|
||||
|
||||
@@ -50,4 +50,7 @@ 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",
|
||||
"draw_termination_labels", "draw_cable_labels", "grid_size", "node_label_items")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, 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
|
||||
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']
|
||||
@@ -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_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 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 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:
|
||||
@@ -163,9 +172,12 @@ class ExportTopoToXML(BaseViewSet, ViewSet):
|
||||
group_virtualchassis=group_virtualchassis,
|
||||
group_id=group_id,
|
||||
straight_cables=straight_cables,
|
||||
draw_termination_labels=draw_termination_labels,
|
||||
draw_cable_labels=draw_cable_labels,
|
||||
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', '
')
|
||||
|
||||
return HttpResponse(xml_data, content_type="application/xml; charset=utf-8")
|
||||
else:
|
||||
@@ -251,3 +263,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
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
+105
-52
@@ -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,
|
||||
@@ -34,9 +35,13 @@ class DeviceFilterForm(
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
||||
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
|
||||
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options")
|
||||
'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_wireless',
|
||||
'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors',
|
||||
'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('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
|
||||
@@ -260,7 +265,12 @@ class DeviceFilterForm(
|
||||
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(
|
||||
label =_('Show Logical Connections'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
@@ -279,24 +289,6 @@ class DeviceFilterForm(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_circuit = forms.NullBooleanField(
|
||||
label=_('Show Circuit Terminations'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_power = forms.NullBooleanField(
|
||||
label=_('Show Power Feeds'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_wireless = forms.NullBooleanField(
|
||||
label =_('Show Wireless Links'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
group_sites = forms.NullBooleanField(
|
||||
label =_('Group Sites'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
@@ -327,6 +319,18 @@ class DeviceFilterForm(
|
||||
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(
|
||||
label=_('Grid Size'),
|
||||
required=False,
|
||||
@@ -335,6 +339,27 @@ 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
|
||||
)
|
||||
|
||||
# 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):
|
||||
fieldsets = (
|
||||
@@ -510,26 +535,32 @@ class IndividualOptionsForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet
|
||||
(
|
||||
'user_id',
|
||||
'ignore_cable_type',
|
||||
'preselected_device_roles',
|
||||
'preselected_tags',
|
||||
'save_coords',
|
||||
'show_unconnected',
|
||||
'show_cables',
|
||||
'show_wireless',
|
||||
'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',
|
||||
),
|
||||
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 '
|
||||
'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(
|
||||
label =_('Show Logical Connections'),
|
||||
required=False,
|
||||
@@ -607,28 +645,6 @@ class IndividualOptionsForm(NetBoxModelForm):
|
||||
'Link peers will be added if \'Show Cables\' is ticked, far-end '
|
||||
'terminations will be added if \'Show Logical Connections\' is ticked.')
|
||||
)
|
||||
show_circuit = forms.BooleanField(
|
||||
label=_('Show Circuit Terminations'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Displays connections between circuit terminations. '
|
||||
'These connections are displayed as blue dashed lines.')
|
||||
)
|
||||
show_power = forms.BooleanField(
|
||||
label=_('Show Power Feeds'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Displays connections between power outlets and power '
|
||||
'ports. These connections are displayed as solid lines in the '
|
||||
'color of the cable. This option depends on \'Show Cables\'.')
|
||||
)
|
||||
show_wireless = forms.BooleanField(
|
||||
label =_('Show Wireless Links'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Displays wireless connections. These connections are '
|
||||
'displayed as blue dotted lines.')
|
||||
)
|
||||
group_sites = forms.BooleanField(
|
||||
label =_('Group Sites'),
|
||||
required=False,
|
||||
@@ -671,6 +687,19 @@ class IndividualOptionsForm(NetBoxModelForm):
|
||||
help_text=_('Enable this option if you want to draw cables as straight lines '
|
||||
'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(
|
||||
label=_('Grid Size'),
|
||||
required=True,
|
||||
@@ -681,13 +710,37 @@ 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.')
|
||||
)
|
||||
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:
|
||||
model = IndividualOptions
|
||||
fields = [
|
||||
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags',
|
||||
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
||||
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power',
|
||||
'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'draw_default_layout',
|
||||
'straight_cables', 'grid_size'
|
||||
'save_coords', 'show_unconnected', 'show_cables', 'show_wireless',
|
||||
'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors',
|
||||
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis',
|
||||
'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),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
@@ -404,10 +405,20 @@ class IndividualOptions(NetBoxModel):
|
||||
straight_cables = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
draw_termination_labels = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
draw_cable_labels = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
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 w=(o,t)=>()=>(o&&(t=o(o=0)),t);var f=(o,t)=>()=>(t||o((t={exports:{}}).exports,t),t.exports);var l=(o,t,e)=>new Promise((r,s)=>{var c=n=>{try{a(e.next(n))}catch(i){s(i)}},g=n=>{try{a(e.throw(n))}catch(i){s(i)}},a=n=>n.done?r(n.value):Promise.resolve(n.value).then(c,g);a((e=e.apply(o,t)).next())});var u,d=w(()=>{u={success:o=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let e=t.querySelector("span");e.textContent=o,new window.Toast(t).show()},error:o=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let e=t.querySelector("span");e.textContent=o,new window.Toast(t).show()}}});var y=f(p=>{d();var m={};document.querySelector("form#images").addEventListener("submit",o=>l(null,null,function*(){o.preventDefault();try{let t=yield fetch("/"+basePath+"api/plugins/netbox_topology_views/images/save/",{method:"POST",body:JSON.stringify(m),headers:{"X-CSRFToken":window.CSRF_TOKEN,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(o=>{o.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:e,image:r}}=t.currentTarget;m[e]=r;let s=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${e}`);s&&(s.innerHTML=`<img src="${r}" />`)})})});y();})();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
|
||||
"mappings": "mNAAO,GAAM,GAAY,AAAC,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,GAAI,GAAc,KACZ,EAAU,SAAS,OAAO,MAAM,KAEtC,OAAS,GAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,GAAM,GAAS,EAAQ,GAAG,OAE1B,GAAI,EAAO,UAAU,EAAG,EAAK,OAAS,KAAO,EAAO,IAAK,CACrD,EAAc,mBAAmB,EAAO,UAAU,EAAK,OAAS,IAChE,OAIR,MAAO,ICfJ,GAAM,GAAQ,CACjB,QAAS,AAAC,GAAY,CAClB,GAAM,GAAK,SAAS,cAAc,kCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,QAEV,MAAO,AAAC,GAAY,CAChB,GAAM,GAAK,SAAS,cAAc,gCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,SCZd,GAAM,GAAU,GACV,EAAY,EAAU,aAE5B,SAAS,cAAc,eAAe,iBAAiB,SAAU,AAAO,GAAM,0BAC1E,EAAE,iBACF,GAAI,CACA,GAAM,GAAM,KAAM,OAAM,IAAM,SAAW,iDAAkD,CACvF,OAAQ,OACR,KAAM,KAAK,UAAU,GACrB,QAAS,CACL,cAAe,EACf,eAAgB,sBAIxB,GAAI,CAAC,EAAI,GAAI,KAAM,IAAI,OAAM,KAAM,GAAI,QACvC,EAAM,QAAQ,wBACT,EAAP,CACE,QAAQ,IAAI,GACZ,EAAM,MAAM,EAAI,aAIxB,SAAS,iBAAiB,kCAAkC,QAAQ,AAAC,GAAO,CACxE,EAAG,iBAAiB,QAAS,AAAC,GAAM,CA3BxC,MA4BQ,GAAI,CAAE,GAAE,wBAAyB,cAAc,OAC/C,GAAM,CACF,QAAS,CAAE,OAAM,UACjB,EAAE,cAEN,EAAQ,GAAQ,EAEhB,GAAM,GAAS,KAAE,cACZ,QAAQ,eADE,cAET,cAAc,sBAAsB,KAC1C,AAAI,GAAQ,GAAO,UAAY,aAAa",
|
||||
"names": []
|
||||
"sources": ["../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
|
||||
"mappings": "oTAAA,IAAaA,EAAbC,EAAAC,EAAA,KAAaF,EAAQ,CACjB,QAAUG,GAAY,CAClB,IAAMC,EAAK,SAAS,cAAc,gCAAgC,EAClE,GAAI,CAACA,EAAI,OAAO,QAAQ,MAAM,iCAAiC,EAC/D,IAAMC,EAAUD,EAAG,cAAc,MAAM,EACvCC,EAAQ,YAAcF,EACR,IAAI,OAAO,MAAMC,CAAE,EAC3B,KAAK,CACf,EACA,MAAQD,GAAY,CAChB,IAAMC,EAAK,SAAS,cAAc,8BAA8B,EAChE,GAAI,CAACA,EAAI,OAAO,QAAQ,MAAM,iCAAiC,EAC/D,IAAMC,EAAUD,EAAG,cAAc,MAAM,EACvCC,EAAQ,YAAcF,EACR,IAAI,OAAO,MAAMC,CAAE,EAC3B,KAAK,CACf,CACJ,ICjBA,IAAAE,EAAAC,EAAAC,GAAA,CAAAC,IAEA,IAAMC,EAAU,CAAC,EAEjB,SAAS,cAAc,aAAa,EAAE,iBAAiB,SAAiBC,GAAMC,EAAA,sBAC1ED,EAAE,eAAe,EACjB,GAAI,CACA,IAAME,EAAM,MAAM,MAAM,IAAM,SAAW,iDAAkD,CACvF,OAAQ,OACR,KAAM,KAAK,UAAUH,CAAO,EAC5B,QAAS,CACL,cAAe,OAAO,WACtB,eAAgB,kBACpB,CACJ,CAAC,EAED,GAAI,CAACG,EAAI,GAAI,MAAM,IAAI,MAAM,MAAMA,EAAI,KAAK,CAAC,EAC7CC,EAAM,QAAQ,gBAAgB,CAClC,OAASC,EAAK,CACV,QAAQ,IAAIA,CAAG,EACfD,EAAM,MAAMC,EAAI,OAAO,CAC3B,CACJ,EAAC,EAED,SAAS,iBAAiB,gCAAgC,EAAE,QAASC,GAAO,CACxEA,EAAG,iBAAiB,QAAUL,GAAM,CAzBxC,IAAAM,EA0BQ,GAAI,EAAEN,EAAE,yBAAyB,aAAc,OAC/C,GAAM,CACF,QAAS,CAAE,KAAAO,EAAM,MAAAC,CAAM,CAC3B,EAAIR,EAAE,cAEND,EAAQQ,CAAI,EAAIC,EAEhB,IAAMC,GAASH,EAAAN,EAAE,cACZ,QAAQ,WAAW,IADT,YAAAM,EAET,cAAc,sBAAsBC,CAAI,IAC1CE,IAAQA,EAAO,UAAY,aAAaD,CAAK,OACrD,CAAC,CACL,CAAC",
|
||||
"names": ["toast", "init_toast", "__esmMin", "message", "el", "content", "require_images", "__commonJSMin", "exports", "init_toast", "mapping", "e", "__async", "res", "toast", "err", "el", "_a", "role", "image", "button"]
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export const getCookie = (name) => {
|
||||
if (!document.cookie) return
|
||||
|
||||
let cookieValue = null
|
||||
const cookies = document.cookie.split(';')
|
||||
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim()
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === name + '=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return cookieValue
|
||||
}
|
||||
@@ -1,6 +1,43 @@
|
||||
import { DataSet } from 'vis-data/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 = {
|
||||
interaction: {
|
||||
@@ -36,9 +73,6 @@ const options = {
|
||||
}
|
||||
}
|
||||
|
||||
// Load CSRF token
|
||||
const csrftoken = getCookie('csrftoken')
|
||||
|
||||
// Render vis graph
|
||||
let graph = null // vis graph instance
|
||||
|
||||
@@ -66,7 +100,21 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||
const edges = new DataSet(
|
||||
topologyData.edges.map((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',
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken,
|
||||
'X-CSRFToken': window.CSRF_TOKEN,
|
||||
Accept: '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
|
||||
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 +438,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 +567,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 {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { getCookie } from './csrftoken.js'
|
||||
import { toast } from './toast.js'
|
||||
|
||||
const mapping = {}
|
||||
const csrftoken = getCookie('csrftoken')
|
||||
|
||||
document.querySelector('form#images').addEventListener('submit', async (e) => {
|
||||
e.preventDefault()
|
||||
@@ -11,7 +9,7 @@ document.querySelector('form#images').addEventListener('submit', async (e) => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(mapping),
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken,
|
||||
'X-CSRFToken': window.CSRF_TOKEN,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
+1297
-442
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.5.1",
|
||||
"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.28.1",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"keycharm": "^0.4.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "^8.0.0"
|
||||
|
||||
@@ -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="panel-body">
|
||||
{% if filter_required %}
|
||||
<div class="bg-warning text-white p-2">Nothing to display! Set filters and hit search button or set "Draw Default Layout" in your individual options.</div>
|
||||
{% endif %}
|
||||
{% if empty_result %}
|
||||
<div class="bg-warning text-white p-2">Empty result set! Please review your filter settings.</div>
|
||||
<div class="bg-info text-white p-2">Hint: Some options may produce an empty result. Use at least "Show Unconnected/Cables/Logical Connections".</div>
|
||||
{% endif %}
|
||||
<div id="visgraph"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<a role="button" class="btn btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}&show_cables=True&show_unconnected=True" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||
Topology
|
||||
</a>
|
||||
@@ -196,11 +196,25 @@ def get_query_settings(request):
|
||||
if request.GET["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
|
||||
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, draw_termination_labels, draw_cable_labels, grid_size, node_label_items
|
||||
|
||||
class LinePattern():
|
||||
wireless = [2, 10, 2, 10]
|
||||
|
||||
+102
-36
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from functools import reduce
|
||||
from typing import DefaultDict, Dict, Optional, Union
|
||||
import time
|
||||
from itertools import chain
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
|
||||
@@ -28,7 +27,6 @@ from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic import View
|
||||
from extras.models import Tag, SavedFilter
|
||||
from wireless.models import WirelessLink
|
||||
from utilities.htmx import htmx_partial
|
||||
from netbox.views.generic import (
|
||||
ObjectView,
|
||||
ObjectListView,
|
||||
@@ -78,7 +76,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 +95,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 +147,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 +230,37 @@ 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.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["href"] = device.get_absolute_url()
|
||||
node["image"] = get_image_for_entity(device)
|
||||
node["label_num_lines"] = len(label_items)
|
||||
|
||||
return node
|
||||
|
||||
@@ -242,6 +270,8 @@ def create_edge(
|
||||
termination_a: Dict,
|
||||
termination_b: Dict,
|
||||
straight_cables: bool,
|
||||
draw_termination_labels: bool,
|
||||
draw_cable_labels: bool,
|
||||
circuit: Optional[Dict] = None,
|
||||
cable: Optional[Cable] = None,
|
||||
wireless: Optional[Dict] = None,
|
||||
@@ -294,10 +324,17 @@ def create_edge(
|
||||
edge["dashes"] = LinePattern().logical
|
||||
edge["color"] = '#f1c232'
|
||||
edge["href"] = interface.get_absolute_url() + "trace"
|
||||
|
||||
|
||||
if cable is not None and hasattr(cable, "label") and cable.label:
|
||||
cable_label = "<br>Label: " + cable.label
|
||||
if draw_cable_labels is True:
|
||||
edge["label"] = cable.label
|
||||
else:
|
||||
cable_label = ""
|
||||
|
||||
edge[
|
||||
"title"
|
||||
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}]"
|
||||
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}] {cable_label}"
|
||||
|
||||
if cable is not None:
|
||||
edge["href"] = cable.get_absolute_url()
|
||||
@@ -307,6 +344,12 @@ def create_edge(
|
||||
# if straight_cables == True: edge["smooth"] = False
|
||||
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
|
||||
|
||||
|
||||
@@ -349,7 +392,10 @@ def get_topology_data(
|
||||
group_virtualchassis: bool,
|
||||
group_id,
|
||||
straight_cables: bool,
|
||||
draw_termination_labels: bool,
|
||||
draw_cable_labels: bool,
|
||||
grid_size: list,
|
||||
node_label_items: list,
|
||||
):
|
||||
|
||||
supported_termination_types = []
|
||||
@@ -402,8 +448,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 (
|
||||
@@ -417,21 +463,21 @@ def get_topology_data(
|
||||
termination_a = {}
|
||||
termination_b = {}
|
||||
circuit_model = {}
|
||||
if circuit_termination.cable is not None:
|
||||
if circuit_termination.cable is not None and bool(circuit_termination.cable.a_terminations) and bool(circuit_termination.cable.b_terminations):
|
||||
termination_a = create_circuit_termination(
|
||||
circuit_termination.cable.a_terminations[0]
|
||||
)
|
||||
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 = {
|
||||
@@ -446,6 +492,8 @@ def get_topology_data(
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
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
|
||||
|
||||
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(
|
||||
@@ -516,6 +564,8 @@ def get_topology_data(
|
||||
termination_b=termination_b,
|
||||
power=True,
|
||||
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
|
||||
|
||||
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(
|
||||
@@ -554,7 +604,7 @@ def get_topology_data(
|
||||
edge_ids += 1
|
||||
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 }
|
||||
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[destination.device.id] = destination.device
|
||||
|
||||
@@ -635,6 +685,8 @@ def get_topology_data(
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
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,
|
||||
wireless=wireless,
|
||||
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 = {}
|
||||
|
||||
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 +783,10 @@ 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, 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
|
||||
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 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']
|
||||
@@ -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 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 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 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:
|
||||
@@ -766,6 +827,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
group_id = request.GET["group"]
|
||||
|
||||
if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true":
|
||||
filter_required = False
|
||||
|
||||
topo_data = get_topology_data(
|
||||
queryset=self.queryset,
|
||||
individualOptions=individualOptions,
|
||||
@@ -785,13 +848,19 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
group_virtualchassis=group_virtualchassis,
|
||||
group_id=group_id,
|
||||
straight_cables=straight_cables,
|
||||
draw_termination_labels=draw_termination_labels,
|
||||
draw_cable_labels=draw_cable_labels,
|
||||
grid_size=grid_size,
|
||||
node_label_items=node_label_items,
|
||||
)
|
||||
|
||||
if topo_data is None or not topo_data["nodes"]:
|
||||
empty_result = True
|
||||
|
||||
else:
|
||||
# No GET-Request in URL. We most likely came here from the navigation menu.
|
||||
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(', ')
|
||||
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_virtualchassis: q['group_virtualchassis'] = "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
|
||||
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:
|
||||
@@ -823,20 +898,6 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
query_string = q.urlencode()
|
||||
return HttpResponseRedirect(f"{request.path}?{query_string}")
|
||||
|
||||
|
||||
if htmx_partial(request):
|
||||
return render(
|
||||
request,
|
||||
"netbox_topology_views/htmx_topology.html",
|
||||
{
|
||||
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
|
||||
"topology_data": json.dumps(topo_data),
|
||||
"broken_image": find_image_url("role-unknown"),
|
||||
"epoch": int(time.time()),
|
||||
"basepath": settings.BASE_PATH,
|
||||
},
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"netbox_topology_views/index.html",
|
||||
@@ -846,6 +907,8 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
||||
"broken_image": find_image_url("role-unknown"),
|
||||
"model": self.model,
|
||||
"basepath": settings.BASE_PATH,
|
||||
"filter_required": filter_required,
|
||||
"empty_result": empty_result,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1175,7 +1238,10 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
|
||||
'group_virtualchassis': queryset.group_virtualchassis,
|
||||
'draw_default_layout': queryset.draw_default_layout,
|
||||
'straight_cables': queryset.straight_cables,
|
||||
'draw_termination_labels': queryset.draw_termination_labels,
|
||||
'draw_cable_labels': queryset.draw_cable_labels,
|
||||
'grid_size': queryset.grid_size,
|
||||
'node_label_items': tuple(queryset.node_label_items.translate({ord(i): None for i in '[]\''}).split(', ')),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ long_description = readme.read_text()
|
||||
|
||||
setup(
|
||||
name="netbox-topology-views",
|
||||
version="4.0.1",
|
||||
description="An NetBox plugin to create Topology maps",
|
||||
version="4.5.1",
|
||||
description="A NetBox plugin to create Topology maps",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/mattieserver/netbox-topology-views",
|
||||
|
||||
Reference in New Issue
Block a user