Compare commits

..
24 Commits
Author SHA1 Message Date
mattieserver f4b1e34826 fix name of devices (#567)
* fix name of devices

* upgrade version
2024-09-11 17:00:15 +02:00
mattieserver f8c6e9e193 bump version (#558) 2024-08-26 12:21:00 +02:00
mattieserver 19adcab4a9 set BaseViewSet (#556) 2024-08-26 12:01:42 +02:00
Mario d2d2106246 Closes #163: Show raster and snap icons (#550)
* proof of concept

* add grid size to forms and individual options
2024-08-20 12:02:45 +02:00
Mario 9072faf7cf Closes #485: Add Ignore Termination Types to "regular" filters (#546)
* add ignore_cable_type filter

* deleted commented command

* corrected saved filter for ignore_cable_type
2024-08-09 15:09:16 +02:00
Mario 6f43b959bd Closes #497: Group by Virtual Chassis (#542)
* add virtual chassis to js

* add virtual chassis to py and migrations
2024-07-29 12:15:04 +02:00
Mario 1048feff52 Fixes #464: Topology not showing properly when using other language than english (#541)
* Change termination type name to model

* change CHOICES to model names

* Change termination type name to model

* change CHOICES to model names
2024-07-29 10:32:30 +02:00
Mario 7ff0a9650f Fixed another mistake (#540) 2024-07-28 00:07:00 +02:00
Mario 8d5de5c49f fix saved filter params not defined (#539) 2024-07-27 22:26:18 +02:00
Mario 96afdf8d83 fix coordinate group in saved filter (#536) 2024-07-24 22:22:13 +02:00
aba0598947 Closes #436 (and #122?): Add options to disable smooth cables and physics (#488)
* allow disabling smooth cables and render them as straight lines instead

* rename 'disable_smooth_cables' to 'straight_cables'

* missed one in renaming

* fix inverted logic

* add GET param and option to DeviceFilterForm

* fix syntax error

* rename var

* add missing query field

* netbox v4 support

---------

Co-authored-by: Daniel Bremer <daniel.bremer@mpsd.mpg.de>
Co-authored-by: mvanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
2024-07-08 15:37:18 +02:00
mattieserver 142de460b9 added restrict to query (#522) 2024-07-04 21:01:40 +02:00
mattieserver 2600209724 4.0 release (#513) 2024-06-24 20:10:05 +02:00
mattieserver ed8a717bc5 seconnd beta (#511) 2024-06-21 19:21:20 +02:00
mattieserver 0b7046b10d fix route name (#508) 2024-06-17 21:43:03 +02:00
mattieserver e784e717ca fix coordSaveCheckbox (#510) 2024-06-17 21:42:47 +02:00
2895294903 477 prepare for netbox 4.0 (#495)
* Change extras.plugins to netbox.plugins (#487)

* Remove ButtonColorChoices (#491)

* remove is_htmx

* fieldset update

* Additions to 477 prepare for netbox 4.0 (#504)

* Re-add overriding of default_renderer

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* Restore i18n of field names

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* Remove unnecessary FieldSets

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* Remove trailing whitespaces in forms.py

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* Fix error in resizeCanvas

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* Remove btn-sm class from buttons

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* Use htmx_partial instead of request.htmx

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

---------

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>

* fix choises

* fix color switcher (use new attribute)

* fix htmx filter

* 4.0.0-beta.1 release

---------

Signed-off-by: Iain Buclaw <ibuclaw@gdcproject.org>
Co-authored-by: Mario <github@franzbonenkamp.de>
Co-authored-by: Iain Buclaw <ibuclaw@gdcproject.org>
2024-06-05 13:23:34 +02:00
mattieserver 8368ffa2fd prepare 3.9.1 (#480) 2024-04-19 15:55:04 +02:00
Mario 23428c8ad2 set models to private (#476) 2024-04-01 23:26:46 +02:00
Mario c00b8c2cfe Fixes #236: Saved filter doesn't reload show cables (#475)
* fix restore saved filter options

* fix restore saved filter group options

* fix htmx export to xml
2024-03-30 17:29:26 +01:00
Mario 32003e59f9 fix power feeds attributeerror (#474) 2024-03-30 11:17:04 +01:00
Mario bd32642b5c refine group frame color (#472) 2024-03-30 10:13:29 +01:00
Mario f463804087 added group options to api (#466) 2024-03-08 16:19:55 +01:00
Mario b92b012309 Closes #329: Draw rectangles around Sites/Locations/Racks (#462) 2024-03-03 17:52:44 +01:00
25 changed files with 1001 additions and 696 deletions
+2
View File
@@ -50,6 +50,8 @@ python3 manage.py collectstatic --no-input
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| >= 4.1.0 | >= v4.1.0 |
| >= 4.0.0 | >= v4.0.0 |
| >= 3.7.0 | >= v3.9.0 |
| >= 3.6.4 | >= v3.8.1 |
| >= 3.6.0 | >= v3.7.X |
+2 -2
View File
@@ -1,11 +1,11 @@
from extras.plugins import PluginConfig
from netbox.plugins import PluginConfig
class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "3.9.0"
version = "4.1.0"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
+1 -1
View File
@@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "draw_default_layout")
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")
+1 -1
View File
@@ -6,6 +6,6 @@ router = NetBoxRouter()
router.register("save-coords", views.SaveCoordsViewSet)
router.register("images", views.SaveRoleImageViewSet)
router.register("xml-export", views.ExportTopoToXML)
router.register("xml-export", views.ExportTopoToXML, "xml-export")
urlpatterns = router.urls
+46 -8
View File
@@ -1,8 +1,11 @@
from typing import Dict
import sys
from netbox.api.viewsets import BaseViewSet
from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
from extras.models import SavedFilter
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
@@ -21,7 +24,7 @@ from netbox_topology_views.views import get_topology_data
from netbox_topology_views.utils import get_image_from_url, export_data_to_xml, get_query_settings
from netbox_topology_views.filters import DeviceFilterSet
class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
class SaveCoordsViewSet(BaseViewSet, ReadOnlyModelViewSet):
permission_required = 'netbox_topology_views.change_coordinate'
queryset = Device.objects.none()
@@ -88,16 +91,14 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
return Response({"status": "saved coords"})
class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
permission_required = ("dcim.view_site", "dcim.view_device")
class ExportTopoToXML(BaseViewSet, ViewSet):
queryset = Device.objects.none()
serializer_class = TopologyDummySerializer
def list(self, request):
self.filterset = DeviceFilterSet
self.queryset = Device.objects.all().select_related(
self.queryset = Device.objects.restrict(request.user, 'view').select_related(
"device_type", "role"
)
self.queryset = self.filterset(request.GET, self.queryset).qs
@@ -108,14 +109,45 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
if request.GET:
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, grid_size = 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"] != '':
try:
saved_filter = SavedFilter.objects.get(pk=filter_id)
saved_filter_params = getattr(saved_filter, 'parameters')
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_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']
if show_single_cable_logical_conns == False and 'show_single_cable_logical_conns' in saved_filter_params: show_single_cable_logical_conns = saved_filter_params['show_single_cable_logical_conns']
if show_cables == False and 'show_cables' in saved_filter_params: show_cables = saved_filter_params['show_cables']
if show_wireless == False and 'show_wireless' in saved_filter_params: show_wireless = saved_filter_params['show_wireless']
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations']
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 grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size']
except SavedFilter.DoesNotExist: # filter_id not found
pass
except Exception as inst:
print(type(inst))
if 'group' not in request.query_params:
group_id = "default"
if 'saved_filter_params' in locals() and "group" in saved_filter_params:
group_id = saved_filter_params['group'][0]
else:
group_id = "default"
else:
group_id = request.query_params["group"]
topo_data = get_topology_data(
queryset=self.queryset,
individualOptions=individualOptions,
ignore_cable_type=ignore_cable_type,
save_coords=save_coords,
show_unconnected=show_unconnected,
show_cables=show_cables,
@@ -125,7 +157,13 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
show_circuit=show_circuit,
show_power=show_power,
show_wireless=show_wireless,
group_sites=group_sites,
group_locations=group_locations,
group_racks=group_racks,
group_virtualchassis=group_virtualchassis,
group_id=group_id,
straight_cables=straight_cables,
grid_size=grid_size,
)
xml_data = export_data_to_xml(topo_data).decode('utf-8')
@@ -135,7 +173,7 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
{"status": "Missing or malformed request parameters"}, status=400
)
class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
class SaveRoleImageViewSet(BaseViewSet, ReadOnlyModelViewSet):
queryset = DeviceRole.objects.none()
serializer_class = RoleImageSerializer
permission_required = (
+209 -79
View File
@@ -15,6 +15,7 @@ from extras.forms import LocalConfigContextFilterForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm
from utilities.forms.rendering import FieldSet
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
from utilities.forms.fields import (
TagFilterField,
@@ -29,26 +30,26 @@ class DeviceFilterForm(
ContactModelFilterForm,
NetBoxModelFilterSetForm
):
default_renderer = forms.renderers.DjangoTemplates()
model = Device
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Options'), (
'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
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',
)),
(_('Device'), ('id',)),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
(_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
(_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)),
(_('Miscellaneous'), (
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
)),
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options")
),
FieldSet('id', name=_("Device")),
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")),
FieldSet('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address', name=_("Operation")),
FieldSet('manufacturer_id', 'device_type_id', 'platform_id', name=_("Hardware")),
FieldSet('tenant_group_id', 'tenant_id', name=_("Tenant")),
FieldSet('contact', 'contact_role', 'contact_group', name=_("Contacts")),
FieldSet(
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', name=_("Components")
),
FieldSet(
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', name=_("Miscellaneous")
),
)
group = forms.ModelChoiceField(
queryset=CoordinateGroup.objects.all(),
@@ -71,8 +72,8 @@ class DeviceFilterForm(
},
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
required=False,
queryset=Region.objects.all(),
required=False,
label=_('Region')
)
site_group_id = DynamicModelMultipleChoiceField(
@@ -111,13 +112,13 @@ class DeviceFilterForm(
label=_('Rack'),
)
status = forms.MultipleChoiceField(
choices=DeviceStatusChoices,
required=False,
choices=DeviceStatusChoices,
required=False,
label=_('Device Status')
)
role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
queryset=DeviceRole.objects.all(),
required=False,
label=_('Role')
)
airflow = forms.MultipleChoiceField(
@@ -136,7 +137,7 @@ class DeviceFilterForm(
mac_address = forms.CharField(
required=False,
label=_('MAC address')
)
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
required=False,
@@ -212,7 +213,7 @@ class DeviceFilterForm(
)
has_oob_ip = forms.NullBooleanField(
required=False,
label='Has an OOB IP',
label=_('Has an OOB IP'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
@@ -228,40 +229,116 @@ class DeviceFilterForm(
tag = TagFilterField(model)
# options
save_coords = forms.BooleanField(
ignore_cable_type = forms.MultipleChoiceField(
label=_('Ignore Termination Types'),
required=False,
choices=IndividualOptions.CHOICES
)
save_coords = forms.NullBooleanField(
label=_('Save Coordinates'),
required=False,
disabled=(not settings.PLUGINS_CONFIG['netbox_topology_views']['allow_coordinates_saving'] or settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates']),
initial=(settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates'])
initial=(settings.PLUGINS_CONFIG['netbox_topology_views']['always_save_coordinates']),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_unconnected = forms.BooleanField(
label=_('Show Unconnected'), required=False, initial=False
show_unconnected = forms.NullBooleanField(
label=_('Show Unconnected'),
required=False,
initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_cables = forms.BooleanField(
label =_('Show Cables'), required=False, initial=False
show_cables = forms.NullBooleanField(
required=False,
initial=False,
label=_('Show Cables'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_logical_connections = forms.BooleanField(
label =_('Show Logical Connections'), required=False, initial=False
show_logical_connections = forms.NullBooleanField(
label =_('Show Logical Connections'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_single_cable_logical_conns = forms.BooleanField(
label =_('Show redundant Cable and Logical Connection'), required=False, initial=False
show_single_cable_logical_conns = forms.NullBooleanField(
label =_('Show redundant Cable and Logical Connection'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_neighbors = forms.BooleanField(
label =_('Show Neighbors'), required=False, initial=False
show_neighbors = forms.NullBooleanField(
label =_('Show Neighbors'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_circuit = forms.BooleanField(
label=_('Show Circuit Terminations'), required=False, initial=False
show_circuit = forms.NullBooleanField(
label=_('Show Circuit Terminations'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_power = forms.BooleanField(
label=_('Show Power Feeds'), required=False, initial=False
show_power = forms.NullBooleanField(
label=_('Show Power Feeds'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
show_wireless = forms.BooleanField(
label =_('Show Wireless Links'), required=False, initial=False
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(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
group_locations = forms.NullBooleanField(
label =_('Group Locations'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
group_racks = forms.NullBooleanField(
label =_('Group Racks'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
group_virtualchassis = forms.NullBooleanField(
label =_('Group Virtual Chassis'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
straight_cables = forms.BooleanField(
label=_('Straight Cables'), required=False, initial=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
grid_size = forms.IntegerField(
label=_('Grid Size'),
required=False,
initial=0,
min_value=0,
max_value=1000,
help_text=_('Show grid and snap dragged icons to grid. Set to 0 to disable grid and snapping.')
)
class CoordinateGroupsForm(NetBoxModelForm):
fieldsets = (
('Group Details', ('name', 'description')),
FieldSet('name', 'description', name=_("Group Details")),
)
class Meta:
@@ -275,7 +352,7 @@ class CoordinateGroupsImportForm(NetBoxModelImportForm):
class CircuitCoordinatesForm(NetBoxModelForm):
fieldsets = (
('CircuitCoordinate', ('group', 'device', 'x', 'y')),
FieldSet('group', 'device', 'x', 'y', name=_("Circuit Coordinate")),
)
class Meta:
@@ -284,7 +361,7 @@ class CircuitCoordinatesForm(NetBoxModelForm):
class PowerPanelCoordinatesForm(NetBoxModelForm):
fieldsets = (
('PowerPanel', ('group', 'device', 'x', 'y')),
FieldSet('group', 'device', 'x', 'y', name=_("Power Panel")),
)
class Meta:
@@ -293,7 +370,7 @@ class PowerPanelCoordinatesForm(NetBoxModelForm):
class PowerFeedCoordinatesForm(NetBoxModelForm):
fieldsets = (
('PowerFeedCoordinate', ('group', 'device', 'x', 'y')),
FieldSet('group', 'device', 'x', 'y', name=_("PowerFeed Coordinate")),
)
class Meta:
@@ -302,7 +379,7 @@ class PowerFeedCoordinatesForm(NetBoxModelForm):
class CoordinatesForm(NetBoxModelForm):
fieldsets = (
('Coordinate', ('group', 'device', 'x', 'y')),
FieldSet('group', 'device', 'x', 'y', name=_("Coordinate")),
)
class Meta:
@@ -332,8 +409,8 @@ class CoordinatesImportForm(NetBoxModelImportForm):
class CircuitCoordinatesFilterForm(NetBoxModelFilterSetForm):
model = CircuitCoordinate
fieldsets = (
(None, ('q', 'filter_id')),
('CircuitCoordinates', ('group', 'device', 'x', 'y'))
FieldSet('q', 'filter_id'),
FieldSet('group', 'device', 'x', 'y', name=_("Circuit Coordinates")),
)
group = forms.ModelMultipleChoiceField(
@@ -357,8 +434,8 @@ class CircuitCoordinatesFilterForm(NetBoxModelFilterSetForm):
class PowerPanelCoordinatesFilterForm(NetBoxModelFilterSetForm):
model = PowerPanelCoordinate
fieldsets = (
(None, ('q', 'filter_id')),
('PowerPanelCoordinates', ('group', 'device', 'x', 'y'))
FieldSet('q', 'filter_id'),
FieldSet('group', 'device', 'x', 'y', name=_('PowerPanel Coordinates')),
)
group = forms.ModelMultipleChoiceField(
@@ -382,8 +459,8 @@ class PowerPanelCoordinatesFilterForm(NetBoxModelFilterSetForm):
class PowerFeedCoordinatesFilterForm(NetBoxModelFilterSetForm):
model = Coordinate
fieldsets = (
(None, ('q', 'filter_id')),
('PowerFeedCoordinates', ('group', 'device', 'x', 'y'))
FieldSet('q', 'filter_id'),
FieldSet('group', 'device', 'x', 'y', name=_("PowerFeed Coordinates")),
)
group = forms.ModelMultipleChoiceField(
@@ -407,8 +484,8 @@ class PowerFeedCoordinatesFilterForm(NetBoxModelFilterSetForm):
class CoordinatesFilterForm(NetBoxModelFilterSetForm):
model = Coordinate
fieldsets = (
(None, ('q', 'filter_id')),
('Coordinates', ('group', 'device', 'x', 'y'))
FieldSet('q', 'filter_id'),
FieldSet('group', 'device', 'x', 'y', name=_("Coordinates")),
)
group = forms.ModelMultipleChoiceField(
@@ -431,8 +508,7 @@ class CoordinatesFilterForm(NetBoxModelFilterSetForm):
class IndividualOptionsForm(NetBoxModelForm):
fieldsets = (
(
None,
FieldSet
(
'user_id',
'ignore_cable_type',
@@ -447,16 +523,21 @@ class IndividualOptionsForm(NetBoxModelForm):
'show_circuit',
'show_power',
'show_wireless',
'group_sites',
'group_locations',
'group_racks',
'group_virtualchassis',
'draw_default_layout',
'straight_cables',
'grid_size',
),
),
)
user_id = forms.CharField(widget=forms.HiddenInput())
ignore_cable_type = forms.MultipleChoiceField(
label=_('Ignore Termination Types'),
required=False,
label=_('Ignore Termination Types'),
required=False,
choices=IndividualOptions.CHOICES,
help_text=_('Choose Termination Types that you want to be ignored. '
'If any ignored Termination Type is part of a connection, the '
@@ -477,8 +558,8 @@ class IndividualOptionsForm(NetBoxModelForm):
'preselected in the filter tab.')
)
save_coords = forms.BooleanField(
label=_('Save Coordinates'),
required=False,
label=_('Save Coordinates'),
required=False,
initial=False,
help_text=_('Coordinates of nodes will be saved if dragged to a different '
'position. This option depends on parameters set in the config file. '
@@ -486,24 +567,24 @@ class IndividualOptionsForm(NetBoxModelForm):
' \'always_save_coordinates\' has been set.')
)
show_unconnected = forms.BooleanField(
label=_('Show Unconnected'),
required=False,
label=_('Show Unconnected'),
required=False,
initial=False,
help_text=_('Draws devices that have no connections or for which no '
'connection is displayed. This option depends on other parameters '
'like \'Show Cables\' and \'Show Logical Connections\'.')
)
show_cables = forms.BooleanField(
label =_('Show Cables'),
required=False,
label =_('Show Cables'),
required=False,
initial=False,
help_text=_('Displays connections between interfaces that are connected '
'with one or more cables. These connections are displayed as solid '
'lines in the color of the cable.')
)
show_logical_connections = forms.BooleanField(
label =_('Show Logical Connections'),
required=False,
label =_('Show Logical Connections'),
required=False,
initial=False,
help_text=_('Displays connections between devices that are not '
'directly connected (e.g. via patch panels). These connections '
@@ -519,35 +600,63 @@ class IndividualOptionsForm(NetBoxModelForm):
'has an effect if \'Show Logical Connections\' is activated.')
)
show_neighbors = forms.BooleanField(
label =_('Show Neighbors'),
required=False,
label =_('Show Neighbors'),
required=False,
initial=False,
help_text=_('Adds neighbors to the filter result set automatically. '
'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,
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,
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,
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,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same site.')
)
group_locations = forms.BooleanField(
label =_('Group Locations'),
required=False,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same location.')
)
group_racks = forms.BooleanField(
label =_('Group Racks'),
required=False,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same rack.')
)
group_virtualchassis = forms.BooleanField(
label =_('Group Virtual Chassis'),
required=False,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same virtual chassis.')
)
draw_default_layout = forms.BooleanField(
label = ('Draw Default Layout'),
required=False,
@@ -555,9 +664,30 @@ class IndividualOptionsForm(NetBoxModelForm):
help_text=_('Enable this option if you want to draw the topology on '
'the initial load (when you go to the topology plugin page).')
)
straight_cables = forms.BooleanField(
label=('Use Straight Cables'),
required=False,
initial=False,
help_text=_('Enable this option if you want to draw cables as straight lines '
'instead of curves.')
)
grid_size = forms.IntegerField(
label=_('Grid Size'),
required=True,
initial=0,
min_value=0,
max_value=1000,
help_text=_('Default grid value. Set to 0 to disable grid. '
'Integers between 0 and 1000 are allowed. Snap to grid will be '
'automatically enabled for values > 0.')
)
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', 'draw_default_layout'
]
'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'
]
@@ -0,0 +1,28 @@
# Generated by Django 4.2.10 on 2024-03-02 16:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0006_powerpanelcoordinate_powerfeedcoordinate_and_more'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='group_locations',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='individualoptions',
name='group_racks',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='individualoptions',
name='group_sites',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.2.10 on 2024-04-29 14:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0007_individualoptions_group_locations_and_more'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='straight_cables',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-07-29 08:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0008_straight_cables'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='group_virtualchassis',
field=models.BooleanField(default=False),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2024-08-14 09:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('netbox_topology_views', '0009_individualoptions_group_virtualchassis'),
]
operations = [
migrations.AddField(
model_name='individualoptions',
name='grid_size',
field=models.PositiveSmallIntegerField(default=0),
),
]
+38 -6
View File
@@ -41,6 +41,8 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
__role: Optional[Role] = None
_netbox_private = True
@property
def role(self) -> Role:
if self.__role:
@@ -111,6 +113,8 @@ class CoordinateGroup(NetBoxModel):
blank = True,
)
_netbox_private = True
class Meta:
ordering = ['name']
@@ -138,6 +142,8 @@ class Coordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -187,6 +193,8 @@ class CircuitCoordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -236,6 +244,8 @@ class PowerPanelCoordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -285,6 +295,8 @@ class PowerFeedCoordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -319,12 +331,12 @@ class PowerFeedCoordinate(NetBoxModel):
class IndividualOptions(NetBoxModel):
CHOICES = (
('interface', 'interface'),
('front port', 'front port'),
('rear port', 'rear port'),
('power outlet', 'power outlet'),
('power port', 'power port'),
('console port', 'console port'),
('console server port', 'console server port'),
('frontport', 'frontport'),
('rearport', 'rearport'),
('poweroutlet', 'poweroutlet'),
('powerport', 'powerport'),
('consoleport', 'consoleport'),
('consoleserverport', 'consoleserverport'),
)
user_id = models.IntegerField(
@@ -374,9 +386,29 @@ class IndividualOptions(NetBoxModel):
show_wireless = models.BooleanField(
default=False
)
group_sites = models.BooleanField(
default=False
)
group_locations = models.BooleanField(
default=False
)
group_racks = models.BooleanField(
default=False
)
group_virtualchassis = models.BooleanField(
default=False
)
draw_default_layout = models.BooleanField(
default=False
)
straight_cables = models.BooleanField(
default=False
)
grid_size = models.PositiveSmallIntegerField(
default=0
)
_netbox_private = True
def __str___(self):
return f"{self.user_id}"
+1 -12
View File
@@ -1,19 +1,16 @@
from extras.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
from utilities.choices import ButtonColorChoices
from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
coordinategroup_buttons = (
PluginMenuButton(
link='plugins:netbox_topology_views:coordinategroup_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinategroup']
),
PluginMenuButton(
link='plugins:netbox_topology_views:coordinategroup_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinategroup']
)
)
@@ -23,14 +20,12 @@ circuitcoordinate_buttons = (
link='plugins:netbox_topology_views:circuitcoordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:circuitcoordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
@@ -40,14 +35,12 @@ powerpanelcoordinate_buttons = (
link='plugins:netbox_topology_views:powerpanelcoordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:powerpanelcoordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
@@ -57,14 +50,12 @@ powerfeedcoordinate_buttons = (
link='plugins:netbox_topology_views:powerfeedcoordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:powerfeedcoordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
@@ -74,14 +65,12 @@ coordinate_buttons = (
link='plugins:netbox_topology_views:coordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
permissions=['netbox_topology_views.add_coordinate']
),
PluginMenuButton(
link='plugins:netbox_topology_views:coordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
permissions=['netbox_topology_views.add_coordinate']
)
)
File diff suppressed because one or more lines are too long
+343 -6
View File
@@ -69,12 +69,136 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
title: htmlTitle(node.title)
}))
)
const group_sites = topologyData.options.group_sites
const group_locations = topologyData.options.group_locations
const group_racks = topologyData.options.group_racks
const group_virtualchassis = topologyData.options.group_virtualchassis
const gridSize = parseInt(topologyData.options.grid_size[0]);
var dragMode = false;
graph = new Network(container, { nodes, edges }, options)
graph.fit()
function getGridPosition(nodeId, gridSize) {
x = graph.getPosition(nodeId).x;
y = graph.getPosition(nodeId).y;
if(x >= 0) {
if((x % gridSize) > (gridSize / 2)) {
x += gridSize;
}
}
else {
if((-x % gridSize) > (gridSize / 2)) {
x -= gridSize;
}
}
x = x - x % gridSize;
if(y >= 0) {
if((y % gridSize) > (gridSize / 2)) {
y += gridSize;
}
}
else {
if((-y % gridSize) > (gridSize / 2)) {
y -= gridSize;
}
}
y = y - y % gridSize;
return {
x: x,
y: y
};
}
function drawGrid(canvascontext) {
// Canvas can be zoomed. It then contains more or less virtual pixels than the real number of pixels
const zoomFactor = graph.getScale() * window.devicePixelRatio;
const virtualWidth = canvascontext.canvas.width / zoomFactor;
const virtualHeight = canvascontext.canvas.height / zoomFactor;
// Canvas can be moved. Get the center of the virtual canvas. Take the grid into account
const virtualCenter = graph.getViewPosition();
const rasterizedCenterX = virtualCenter.x - virtualCenter.x % gridSize;
const rasterizedCenterY = virtualCenter.y - virtualCenter.y % gridSize;
// Calculate virtual space for the grid
const hSpace = (virtualWidth / 2) - (virtualWidth / 2) % gridSize + gridSize;
const vSpace = (virtualHeight / 2) - (virtualHeight / 2) % gridSize + gridSize;
// Calculate virtual position for the grid
const left = rasterizedCenterX - gridSize - hSpace;
const right = rasterizedCenterX + gridSize + hSpace;
const top = rasterizedCenterY - gridSize - vSpace;
const bottom = rasterizedCenterY + gridSize + vSpace;
// Draw grid
canvascontext.beginPath();
for (let x = left; x < right; x += gridSize) {
canvascontext.moveTo(x, top);
canvascontext.lineTo(x, bottom);
}
for (let y = top; y < bottom; y += gridSize) {
canvascontext.moveTo(left, y);
canvascontext.lineTo(right, y);
}
canvascontext.strokeStyle = '#777777';
canvascontext.stroke();
}
function drawGridSnapHint(canvascontext) {
// Draw grid hinting line and circle
if(gridSize > 0 && dragMode == true && graph.getSelectedNodes().length > 0) {
for(i = 0; i < graph.getSelectedNodes().length; i++) {
id = graph.getSelectedNodes()[i];
if(window.nodes.get(id).x != graph.getPosition(id).x || window.nodes.get(id).y != graph.getPosition(id).y) {
pos = getGridPosition(graph.getSelectedNodes()[i], gridSize);
canvascontext.beginPath();
canvascontext.arc(graph.getPosition(graph.getSelectedNodes()[i]).x, graph.getPosition(graph.getSelectedNodes()[i]).y, 5, 0, 2 * Math.PI);
canvascontext.fillStyle = '#FF3D3D';
canvascontext.fill();
canvascontext.beginPath();
canvascontext.moveTo(graph.getPosition(graph.getSelectedNodes()[i]).x, graph.getPosition(graph.getSelectedNodes()[i]).y);
canvascontext.lineTo(pos.x, pos.y);
canvascontext.strokeStyle = '#FF3D3D';
canvascontext.stroke();
canvascontext.beginPath();
canvascontext.arc(pos.x, pos.y, 10, 0, 2 * Math.PI);
canvascontext.fillStyle = '#9C0000';
canvascontext.fill();
}
}
}
}
graph.on('dragStart', (params) => {
dragMode = true;
})
graph.on('dragEnd', (params) => {
if (coordSaveCheckbox == null) return
if (!coordSaveCheckbox.checked) return
dragMode = false;
// Place icon on the grid
if(gridSize > 0 && graph.getSelectedNodes().length > 0) {
for(i = 0; i < graph.getSelectedNodes().length; i++) {
id = graph.getSelectedNodes()[i];
if(window.nodes.get(id).x != graph.getPosition(id).x || window.nodes.get(id).y != graph.getPosition(id).y) {
pos = getGridPosition(graph.getSelectedNodes()[i], gridSize);
window.nodes.update({id: graph.getSelectedNodes()[i], x: pos.x, y: pos.y});
}
}
}
if (coordSaveCheckbox.options[coordSaveCheckbox.selectedIndex].text != "Yes") return
Promise.allSettled(
Object.entries(graph.getPositions(params.nodes)).map(
@@ -133,6 +257,219 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
})
}
})
graph.on('beforeDrawing', (canvascontext) => {
if (gridSize > 0) {
drawGrid(canvascontext);
}
})
graph.on('afterDrawing', (canvascontext) => {
allRectangles = [];
if(group_sites != null && group_sites == 'on') { drawGroupRectangles(canvascontext, groupedNodeSites, siteRectParams); }
if(group_locations != null && group_locations == 'on') { drawGroupRectangles(canvascontext, groupedNodeLocations, locationRectParams); }
if(group_racks != null && group_racks == 'on') { drawGroupRectangles(canvascontext, groupedNodeRacks, rackRectParams); }
if(group_virtualchassis != null && group_virtualchassis == 'on') { drawGroupRectangles(canvascontext, groupedNodeVirtualchassis, virtualchassisRectParams); }
drawGridSnapHint(canvascontext);
})
graph.on('click', (canvascontext) => {
allRectangles.forEach(key => {
// Is the mouse pointer inside of the current rectangle?
if(canvascontext.pointer.canvas.x > (key.x1 - key.border / 2 - 3) && canvascontext.pointer.canvas.x < (key.x2 + key.border / 2 + 3)
&& canvascontext.pointer.canvas.y > (key.y1 - key.border / 2 - 3) && canvascontext.pointer.canvas.y < (key.y2 + key.border / 2 + 3)) {
// We just want to react when the border has been clicked, not the whole rectangle
if (canvascontext.pointer.canvas.x < (key.x1 + key.border / 2 + 3) || canvascontext.pointer.canvas.x > (key.x2 - key.border / 2 - 3)
|| canvascontext.pointer.canvas.y < (key.y1 + key.border / 2 + 3) || canvascontext.pointer.canvas.y > (key.y2 - key.border / 2 - 3)) {
// Generate an array of affected nodes in order to pass it to the select.Nodes() function
let arr = [];
if(key.category == "Site") {
groupedNodeSites.forEach(subArray => {
subArray.forEach(element => {
if (element[1] == key.id) {
arr.push(element[0]);
}
});
});
}
if(key.category == "Location") {
groupedNodeLocations.forEach(subArray => {
subArray.forEach(element => {
if (element[1] === key.id) {
arr.push(element[0]);
}
});
});
}
if(key.category == "Rack") {
groupedNodeRacks.forEach(subArray => {
subArray.forEach(element => {
if (element[1] === key.id) {
arr.push(element[0]);
}
});
});
}
if(key.category == "Virtual Chassis") {
groupedNodeVirtualchassis.forEach(subArray => {
subArray.forEach(element => {
if (element[1] === key.id) {
arr.push(element[0]);
}
});
});
}
graph.selectNodes(arr);
}
}
});
})
// Add information on which node belongs to which group (site/location/rack/virtualchassis).
// Create an array for each group in order to loop through that arrays later
function combineNodeInfo(typeId, type) {
let nodesArray = [];
// 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]]);
}
}
// Split single array above into arrays grouped by node id
let groupedNodeArray = nodesArray.reduce((acc, value) => {
let key = value[1]; // node id
acc[key] = acc[key] || [];
acc[key].push(value);
return acc;
}, {});
return Object.values(groupedNodeArray);
}
var allRectangles = [];
/* Draw a single rectangle with given parameters
rectangle expects an object that consists of the following keys:
ctx: canvas context on which the rectangle should be drawn
x: x-coordinate of top left point of the rectangle
y: y-coordinate of top left point of the rectangle
width: width of rectangle
height: height of rectangle
lineWidth: border width
color: border color
text: a string to be placed where you want it to be
textPaddingX: x-position of the text
textPaddingY: y-position of the text
font: text font */
function drawGroupRectangle(rectangle) {
// Draw rectangle
rectangle.ctx.beginPath();
rectangle.ctx.lineWidth = rectangle.lineWidth;
rectangle.ctx.strokeStyle = rectangle.color;
rectangle.ctx.rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
rectangle.ctx.stroke();
// Draw text
rectangle.ctx.font = rectangle.font;
rectangle.ctx.fillStyle = rectangle.color;
rectangle.ctx.fillText(rectangle.text, rectangle.x + rectangle.textPaddingX, rectangle.y + rectangle.textPaddingY);
allRectangles.push({category: rectangle.category, id: rectangle.id, x1: rectangle.x, y1: rectangle.y, x2: rectangle.x + rectangle.width, y2: rectangle.y + rectangle.height, border: rectangle.lineWidth})
}
/* Draw all rectangles of a given group (site/location//virtualchassis)
rectParams expects an object that consists of the following keys:
lineWidth: border width (string)
color: border color (string)
paddingX: rectangle x-padding, calculated from the center of a node (int)
paddingY: rectangle y-padding, calculated from the center of a node (int)
textPaddingX: text x-padding, calculated from the lower left point of the text (int)
textPaddingY: text y-padding, calculated from the lower left point of the text (int)
font: css-like font size and font (string) */
function drawGroupRectangles(canvascontext, groupedNodes, rectParams) {
for(let value of Object.entries(groupedNodes)) {
const rectangles = [];
const xValues = [];
const yValues = [];
for(let val of value[1]) {
xValues.push(graph.getPosition(val[0]).x);
yValues.push(graph.getPosition(val[0]).y);
}
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;
rectangles.push({
ctx: canvascontext,
x: rectX,
y: rectY,
width: rectSizeX,
height: rectSizeY,
lineWidth: rectParams.lineWidth,
color: rectParams.color,
text: value[1][0][2],
textPaddingX: rectParams.textPaddingX,
textPaddingY: rectParams.textPaddingY,
font: rectParams.font,
id: value[1][0][1],
category: rectParams.category
});
rectangles.forEach(function(rectangle) {
drawGroupRectangle(rectangle);
});
}
}
let groupedNodeSites = combineNodeInfo('site_id', 'site');
let siteRectParams = {
lineWidth: "5",
color: "red",
paddingX: 84,
paddingY: 84,
textPaddingX: 8,
textPaddingY: -8,
font: "14px helvetica",
category: "Site"
}
let groupedNodeLocations = combineNodeInfo('location_id', 'location');
let locationRectParams = {
lineWidth: "5",
color: "#337ab7",
paddingX: 77,
paddingY: 77,
textPaddingX: 22,
textPaddingY: 29,
font: "14px helvetica",
category: "Location"
}
let groupedNodeRacks = combineNodeInfo('rack_id', 'rack');
let rackRectParams = {
lineWidth: "5",
color: "green",
paddingX: 70,
paddingY: 70,
textPaddingX: 15,
textPaddingY: 36,
font: "14px helvetica",
category: "Rack"
}
let groupedNodeVirtualchassis = combineNodeInfo('virtual_chassis_id', 'virtual_chassis');
let virtualchassisRectParams = {
lineWidth: "5",
color: "orange",
paddingX: 63,
paddingY: 63,
textPaddingX: 8,
textPaddingY: 43,
font: "14px helvetica",
category: "Virtual Chassis"
}
})()
// Download Graph
@@ -208,17 +545,17 @@ const observer = new MutationObserver((mutations) =>
if (
!graph ||
mutation.type !== 'attributes' ||
mutation.attributeName !== 'data-netbox-color-mode' ||
mutation.attributeName !== 'data-bs-theme' ||
!(mutation.target instanceof HTMLElement)
)
return
const { netboxColorMode } = mutation.target.dataset
const netboxColorMode = mutation.target.dataset.bsTheme
options.nodes.font.color = netboxColorMode === 'dark' ? '#fff' : '#000'
graph.setOptions(options)
})
)
observer.observe(document.documentElement, {
observer.observe(document.body, {
attributes: true,
attributeFilter: ['data-netbox-color-mode']
attributeFilter: ['data-bs-theme']
})
+67 -504
View File
@@ -1,12 +1,12 @@
{
"name": "netbox_topology_views",
"version": "3.9.0",
"lockfileVersion": 2,
"version": "4.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "netbox_topology_views",
"version": "3.9.0",
"version": "4.1.0",
"dependencies": {
"vis-data": "^7.1.9",
"vis-network": "^9.1.9",
@@ -50,9 +50,9 @@
}
},
"node_modules/@types/hammerjs": {
"version": "2.0.41",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
"version": "2.0.45",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.45.tgz",
"integrity": "sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ=="
},
"node_modules/anymatch": {
"version": "3.1.3",
@@ -68,37 +68,34 @@
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -111,14 +108,20 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/esbuild": {
"version": "0.12.29",
@@ -499,9 +502,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -511,9 +514,9 @@
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
@@ -525,10 +528,13 @@
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
@@ -542,22 +548,22 @@
"node": ">= 6"
}
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1"
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4.0"
"node": ">= 0.4"
}
},
"node_modules/immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"dev": true
},
"node_modules/is-binary-path": {
@@ -573,12 +579,15 @@
}
},
"node_modules/is-core-module": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"dependencies": {
"has": "^1.0.3"
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -659,12 +668,12 @@
}
},
"node_modules/resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"dependencies": {
"is-core-module": "^2.11.0",
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -676,9 +685,9 @@
}
},
"node_modules/sass": {
"version": "1.63.6",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
"integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==",
"version": "1.78.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz",
"integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -693,9 +702,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -785,451 +794,5 @@
"component-emitter": "^1.3.0 || ^2.0.0"
}
}
},
"dependencies": {
"@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
"requires": {
"@types/hammerjs": "^2.0.36"
}
},
"@esbuild/linux-loong64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
"integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
"dev": true,
"optional": true
},
"@types/hammerjs": {
"version": "2.0.41",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"dev": true,
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"esbuild": {
"version": "0.12.29",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
"dev": true
},
"esbuild-android-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
"integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
"dev": true,
"optional": true
},
"esbuild-android-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
"integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
"dev": true,
"optional": true
},
"esbuild-darwin-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
"integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
"dev": true,
"optional": true
},
"esbuild-darwin-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
"integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
"dev": true,
"optional": true
},
"esbuild-freebsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
"integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
"dev": true,
"optional": true
},
"esbuild-freebsd-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
"integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
"dev": true,
"optional": true
},
"esbuild-linux-32": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
"integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
"dev": true,
"optional": true
},
"esbuild-linux-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
"dev": true,
"optional": true
},
"esbuild-linux-arm": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
"integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
"dev": true,
"optional": true
},
"esbuild-linux-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
"integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
"dev": true,
"optional": true
},
"esbuild-linux-mips64le": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
"integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
"dev": true,
"optional": true
},
"esbuild-linux-ppc64le": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
"integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
"dev": true,
"optional": true
},
"esbuild-linux-riscv64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
"integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
"dev": true,
"optional": true
},
"esbuild-linux-s390x": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
"integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
"dev": true,
"optional": true
},
"esbuild-netbsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
"integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
"dev": true,
"optional": true
},
"esbuild-openbsd-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
"integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
"dev": true,
"optional": true
},
"esbuild-sass-plugin": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz",
"integrity": "sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==",
"dev": true,
"requires": {
"esbuild": "^0.14.5",
"picomatch": "^2.3.0",
"resolve": "^1.20.0",
"sass": "^1.45.0"
},
"dependencies": {
"esbuild": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
"dev": true,
"requires": {
"@esbuild/linux-loong64": "0.14.54",
"esbuild-android-64": "0.14.54",
"esbuild-android-arm64": "0.14.54",
"esbuild-darwin-64": "0.14.54",
"esbuild-darwin-arm64": "0.14.54",
"esbuild-freebsd-64": "0.14.54",
"esbuild-freebsd-arm64": "0.14.54",
"esbuild-linux-32": "0.14.54",
"esbuild-linux-64": "0.14.54",
"esbuild-linux-arm": "0.14.54",
"esbuild-linux-arm64": "0.14.54",
"esbuild-linux-mips64le": "0.14.54",
"esbuild-linux-ppc64le": "0.14.54",
"esbuild-linux-riscv64": "0.14.54",
"esbuild-linux-s390x": "0.14.54",
"esbuild-netbsd-64": "0.14.54",
"esbuild-openbsd-64": "0.14.54",
"esbuild-sunos-64": "0.14.54",
"esbuild-windows-32": "0.14.54",
"esbuild-windows-64": "0.14.54",
"esbuild-windows-arm64": "0.14.54"
}
}
}
},
"esbuild-sunos-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
"integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
"dev": true,
"optional": true
},
"esbuild-windows-32": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
"integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
"dev": true,
"optional": true
},
"esbuild-windows-64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
"integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
"dev": true,
"optional": true
},
"esbuild-windows-arm64": {
"version": "0.14.54",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
"integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
"dev": true
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-core-module": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true
},
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"keycharm": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
"integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ=="
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"requires": {
"picomatch": "^2.2.1"
}
},
"resolve": {
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
"dev": true,
"requires": {
"is-core-module": "^2.11.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
}
},
"sass": {
"version": "1.63.6",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz",
"integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"vis-data": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz",
"integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==",
"requires": {}
},
"vis-network": {
"version": "9.1.9",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz",
"integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==",
"requires": {}
},
"vis-util": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz",
"integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==",
"requires": {}
}
}
}
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "3.9.0",
"version": "4.1.0",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
+1 -1
View File
@@ -1,5 +1,5 @@
from extras.plugins import PluginTemplateExtension
from netbox.plugins import PluginTemplateExtension
from django.conf import settings
from packaging import version
@@ -8,15 +8,15 @@
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadXml" class="btn btn-sm btn-info" href="#">
<a id="btnDownloadXml" class="btn btn-info" href="#">
<i class="mdi mdi-download"></i>
Download XML
</a>
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<a id="btnDownloadImage" class="btn btn-info" href="#">
<i class="mdi mdi-download"></i>
Download PNG
</a>
<a class="btn btn-sm btn-danger" data-bs-dismiss="modal">
<a class="btn btn-danger" data-bs-dismiss="modal">
<i class="mdi mdi-close-thick"></i>
Close
</a>
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
{% endblock %}
{% block content-wrapper %}
{% block content %}
<form id="images" class="container py-4">
<div class="d-flex flex-wrap justify-content-center gap-3 mb-2">
{% for role in roles %}
@@ -39,7 +39,7 @@
<button class="btn btn-primary" type="submit">Save</button>
</form>
{% include 'netbox_topology_views/toasts.html' %}
{% endblock content-wrapper %}
{% endblock content %}
{% block javascript %}
<script type="text/javascript">
@@ -1,4 +1,4 @@
{% extends 'base/layout.html' %}
{% extends 'generic/_base.html' %}
{% load buttons %}
{% load render_table from django_tables2 %}
{% load static %}
@@ -16,11 +16,11 @@
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadXml" class="btn btn-sm btn-info" href="#">
<a id="btnDownloadXml" class="btn btn-info" href="#">
<i class="mdi mdi-download"></i>
Download XML
</a>
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<a id="btnDownloadImage" class="btn btn-info" href="#">
<i class="mdi mdi-download"></i>
Download PNG
</a>
@@ -29,7 +29,7 @@
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3" id="tabswrapper">
<ul class="nav nav-tabs" id="tabswrapper">
{% block tab_items %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
@@ -48,7 +48,7 @@
</ul>
{% endblock tabs %}
{% block content-wrapper %}
{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content" id="tabcontentwrapper">
@@ -73,7 +73,7 @@
</div>
{% endwith %}
{% endblock content-wrapper %}
{% endblock content %}
{% block javascript %}
<script type="text/javascript">
@@ -85,10 +85,9 @@
resizeCanvas();
function resizeCanvas() {
heightToSubtract = getElementHeight('.navbar') +
getElementHeight('.title-container') +
getElementHeight('#tabswrapper') +
getElementHeight('#filterwrapper') +
heightToSubtract = getElementHeight('.page-header') +
getElementHeight('#tabswrapper') +
getElementHeight('#filterwrapper') +
getElementHeight('.footer') +
getElementPadding('#tabcontentwrapper');
@@ -1,4 +1,4 @@
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?location_id={{ object.id }}&show_cables=on&show_unconnected=on" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
<a role="button" class="btn btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?location_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>
@@ -1,4 +1,4 @@
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}&show_cables=on&show_unconnected=on" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
<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>
+52 -11
View File
@@ -107,9 +107,13 @@ def get_model_role(model: Type[Model]) -> Role:
)
def get_query_settings(request):
filter_id = ''
if "filter_id" in request.GET:
filter_id = request.GET["filter_id"]
save_coords = False
if "save_coords" in request.GET:
if request.GET["save_coords"] == "on":
if request.GET["save_coords"] == "True":
save_coords = True
# General options overrides
if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False:
@@ -119,47 +123,84 @@ def get_query_settings(request):
save_coords = True
# Individual options
ignore_cable_type = ()
if "ignore_cable_type" in request.GET:
ignore_cable_type = request.GET.getlist('ignore_cable_type')
show_unconnected = False
if "show_unconnected" in request.GET:
if request.GET["show_unconnected"] == "on":
if request.GET["show_unconnected"] == "True":
show_unconnected = True
show_power = False
if "show_power" in request.GET:
if request.GET["show_power"] == "on":
if request.GET["show_power"] == "True":
show_power = True
show_circuit = False
if "show_circuit" in request.GET:
if request.GET["show_circuit"] == "on":
if request.GET["show_circuit"] == "True":
show_circuit = True
show_logical_connections = False
if "show_logical_connections" in request.GET:
if request.GET["show_logical_connections"] == "on" :
if request.GET["show_logical_connections"] == "True" :
show_logical_connections = True
show_single_cable_logical_conns = False
if "show_single_cable_logical_conns" in request.GET:
if request.GET["show_single_cable_logical_conns"] == "on" :
if request.GET["show_single_cable_logical_conns"] == "True" :
show_single_cable_logical_conns = True
show_cables = False
if "show_cables" in request.GET:
if request.GET["show_cables"] == "on" :
if request.GET["show_cables"] == "True" :
show_cables = True
show_wireless = False
if "show_wireless" in request.GET:
if request.GET["show_wireless"] == "on" :
if request.GET["show_wireless"] == "True" :
show_wireless = True
group_sites = False
if "group_sites" in request.GET:
if request.GET["group_sites"] == "True" :
group_sites = True
group_locations = False
if "group_locations" in request.GET:
if request.GET["group_locations"] == "True" :
group_locations = True
group_racks = False
if "group_racks" in request.GET:
if request.GET["group_racks"] == "True" :
group_racks = True
group_virtualchassis = False
if "group_virtualchassis" in request.GET:
if request.GET["group_virtualchassis"] == "True" :
group_virtualchassis = True
group = False
if "group" in request.GET:
group = request.GET["group"]
show_neighbors = False
if "show_neighbors" in request.GET:
if request.GET["show_neighbors"] == "on" :
if request.GET["show_neighbors"] == "True" :
show_neighbors = True
return save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors
straight_cables = False
if "straight_cables" in request.GET:
if request.GET["straight_cables"] == "True":
straight_cables = 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
class LinePattern():
wireless = [2, 10, 2, 10]
+120 -28
View File
@@ -4,7 +4,6 @@ from typing import DefaultDict, Dict, Optional, Union
import time
from itertools import chain
from utilities.htmx import is_htmx
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
from dcim.models import (
Cable,
@@ -27,8 +26,9 @@ from django.db.models.functions import Lower
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
from django.shortcuts import render, get_object_or_404
from django.views.generic import View
from extras.models import Tag
from extras.models import Tag, SavedFilter
from wireless.models import WirelessLink
from utilities.htmx import htmx_partial
from netbox.views.generic import (
ObjectView,
ObjectListView,
@@ -92,7 +92,7 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
return RoleImage.objects.get(**query).get_image_url()
except RoleImage.DoesNotExist:
return find_image_url(
entity.device_role.slug if is_device else get_model_slug(entity.__class__)
entity.role.slug if is_device else get_model_slug(entity.__class__)
)
@@ -146,15 +146,15 @@ 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 += (
f"<tr><th>Type: </th><td>{device.device_type.model}</td></tr>"
)
if device.device_role.name is not None:
if device.role.name is not None:
node_content += (
f"<tr><th>Role: </th><td>{device.device_role.name}</td></tr>"
f"<tr><th>Role: </th><td>{device.role.name}</td></tr>"
)
if device.serial != "":
node_content += f"<tr><th>Serial: </th><td>{device.serial}</td></tr>"
@@ -180,8 +180,21 @@ def create_node(
node["id"] = device.pk
if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color
if device.site is not None:
node["site"] = device.site.name
node["site_id"] = device.site_id
if device.location is not None:
node["location"] = device.location.name
node["location_id"] = device.location_id
if device.rack is not None:
node["rack"] = device.rack.name
node["rack_id"] = device.rack_id
if device.virtual_chassis is not None:
node["virtual_chassis"] = device.virtual_chassis.name
node["virtual_chassis_id"] = device.virtual_chassis_id
if device.role.color != "":
node["color.border"] = "#" + device.role.color
model_class = getattr(netbox_topology_views.models, model_name)
@@ -228,6 +241,7 @@ def create_edge(
edge_id: int,
termination_a: Dict,
termination_b: Dict,
straight_cables: bool,
circuit: Optional[Dict] = None,
cable: Optional[Cable] = None,
wireless: Optional[Dict] = None,
@@ -290,6 +304,9 @@ def create_edge(
if hasattr(cable, 'color') and cable.color != "":
edge["color"] = "#" + cable.color
# if straight_cables == True: edge["smooth"] = False
edge["smooth"] = not straight_cables
return edge
@@ -317,6 +334,7 @@ def get_topology_data(
queryset: QuerySet,
individualOptions: IndividualOptions,
show_unconnected: bool,
ignore_cable_type: list,
save_coords: bool,
show_cables: bool,
show_circuit: bool,
@@ -325,7 +343,13 @@ def get_topology_data(
show_neighbors: bool,
show_power: bool,
show_wireless: bool,
group_sites: bool,
group_locations: bool,
group_racks: bool,
group_virtualchassis: bool,
group_id,
straight_cables: bool,
grid_size: list,
):
supported_termination_types = []
@@ -338,6 +362,7 @@ def get_topology_data(
nodes_devices = {}
edges = []
nodes = []
options = {}
edge_ids = 0
nodes_circuits: Dict[int, Circuit] = {}
nodes_powerpanel: Dict[int, PowerPanel] = {}
@@ -346,8 +371,6 @@ def get_topology_data(
cable_ids = DefaultDict(dict)
interface_ids = DefaultDict(dict)
ignore_cable_type = individualOptions.ignore_cable_type
device_ids = [d.pk for d in queryset]
site_ids = [d.site_id for d in queryset]
@@ -422,6 +445,7 @@ def get_topology_data(
circuit=circuit_model,
termination_a=termination_a,
termination_b=termination_b,
straight_cables=straight_cables,
)
)
@@ -491,6 +515,7 @@ def get_topology_data(
termination_a=termination_a,
termination_b=termination_b,
power=True,
straight_cables=straight_cables,
)
)
@@ -529,7 +554,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))
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=interface, straight_cables=straight_cables))
nodes_devices[interface.device.id] = interface.device
nodes_devices[destination.device.id] = destination.device
@@ -539,11 +564,11 @@ def get_topology_data(
).select_related("termination_type")
for link in links:
if link.termination_type.name in ignore_cable_type:
if link.termination_type.model in ignore_cable_type:
continue
# Normal device cables
if link.termination_type.name in supported_termination_types:
if link.termination_type.model in supported_termination_types:
complete_link = False
if link.cable_end == "A":
if link.cable_id not in cable_ids:
@@ -609,6 +634,7 @@ def get_topology_data(
cable=link.cable,
termination_a=termination_a,
termination_b=termination_b,
straight_cables=straight_cables,
)
)
@@ -648,9 +674,23 @@ def get_topology_data(
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
straight_cables=straight_cables,
)
)
if group_locations:
options['group_locations'] = 'on'
if group_racks:
options['group_racks'] = 'on'
if group_sites:
options['group_sites'] = 'on'
if group_virtualchassis:
options['group_virtualchassis'] = 'on'
if grid_size:
options['grid_size'] = grid_size
else:
options['grid_size'] = list('0')
for qs_device in queryset:
if qs_device.pk not in nodes_devices and show_unconnected:
nodes_devices[qs_device.pk] = qs_device
@@ -663,6 +703,7 @@ def get_topology_data(
results["nodes"] = nodes
results["edges"] = edges
results["group"] = group_id
results["options"] = options
return results
@@ -675,7 +716,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
def get(self, request):
self.filterset = DeviceFilterSet
self.queryset = Device.objects.all().select_related(
self.queryset = Device.objects.restrict(request.user, 'view').select_related(
"device_type", "role"
)
self.queryset = self.filterset(request.GET, self.queryset).qs
@@ -688,10 +729,39 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET:
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
filter_id, ignore_cable_type, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, group_virtualchassis, group, show_neighbors, straight_cables, grid_size = 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"] != '':
try:
saved_filter = SavedFilter.objects.get(pk=filter_id)
saved_filter_params = getattr(saved_filter, 'parameters')
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_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']
if show_single_cable_logical_conns == False and 'show_single_cable_logical_conns' in saved_filter_params: show_single_cable_logical_conns = saved_filter_params['show_single_cable_logical_conns']
if show_cables == False and 'show_cables' in saved_filter_params: show_cables = saved_filter_params['show_cables']
if show_wireless == False and 'show_wireless' in saved_filter_params: show_wireless = saved_filter_params['show_wireless']
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations']
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: straight_cables = saved_filter_params['straight_cables']
if grid_size == 0 and 'grid_size' in saved_filter_params: grid_size = saved_filter_params['grid_size']
except SavedFilter.DoesNotExist: # filter_id not found
pass
except Exception as inst:
print(type(inst))
if "group" not in request.GET:
group_id = "default"
if 'saved_filter_params' in locals() and "group" in saved_filter_params:
group_id = saved_filter_params['group'][0]
else:
group_id = "default"
else:
group_id = request.GET["group"]
@@ -699,6 +769,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
topo_data = get_topology_data(
queryset=self.queryset,
individualOptions=individualOptions,
ignore_cable_type=ignore_cable_type,
save_coords=save_coords,
show_unconnected=show_unconnected,
show_cables=show_cables,
@@ -708,37 +779,52 @@ class TopologyHomeView(PermissionRequiredMixin, View):
show_circuit=show_circuit,
show_power=show_power,
show_wireless=show_wireless,
group_sites=group_sites,
group_locations=group_locations,
group_racks=group_racks,
group_virtualchassis=group_virtualchassis,
group_id=group_id,
straight_cables=straight_cables,
grid_size=grid_size,
)
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)
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 = []
q = QueryDict(mutable=True)
q.setlist("role_id", list(preselected_device_roles))
q.setlist("tag", list(preselected_tags))
q.setlist("ignore_cable_type", ignore_cable_type)
if individualOptions.save_coords: q['save_coords'] = "on"
if individualOptions.show_unconnected: q['show_unconnected'] = "on"
if individualOptions.show_cables: q['show_cables'] = "on"
if individualOptions.show_logical_connections: q['show_logical_connections'] = "on"
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on"
if individualOptions.show_neighbors: q['show_neighbors'] = "on"
if individualOptions.show_circuit: q['show_circuit'] = "on"
if individualOptions.show_power: q['show_power'] = "on"
if individualOptions.show_wireless: q['show_wireless'] = "on"
if individualOptions.save_coords: q['save_coords'] = "True"
if individualOptions.show_unconnected: q['show_unconnected'] = "True"
if individualOptions.show_cables: q['show_cables'] = "True"
if individualOptions.show_logical_connections: q['show_logical_connections'] = "True"
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "True"
if individualOptions.show_neighbors: q['show_neighbors'] = "True"
if individualOptions.show_circuit: q['show_circuit'] = "True"
if individualOptions.show_power: q['show_power'] = "True"
if individualOptions.show_wireless: q['show_wireless'] = "True"
if individualOptions.group_sites: q['group_sites'] = "True"
if individualOptions.group_locations: q['group_locations'] = "True"
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.grid_size: q['grid_size'] = individualOptions.grid_size
if individualOptions.draw_default_layout:
q['draw_init'] = "true"
q['draw_init'] = "True"
else:
q['draw_init'] = "false"
q['draw_init'] = "False"
query_string = q.urlencode()
return HttpResponseRedirect(f"{request.path}?{query_string}")
if is_htmx(request):
if htmx_partial(request):
return render(
request,
"netbox_topology_views/htmx_topology.html",
@@ -1083,7 +1169,13 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
'show_circuit': queryset.show_circuit,
'show_power': queryset.show_power,
'show_wireless': queryset.show_wireless,
'group_sites': queryset.group_sites,
'group_locations': queryset.group_locations,
'group_racks': queryset.group_racks,
'group_virtualchassis': queryset.group_virtualchassis,
'draw_default_layout': queryset.draw_default_layout,
'straight_cables': queryset.straight_cables,
'grid_size': queryset.grid_size,
},
)
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup(
name="netbox-topology-views",
version="3.9.0",
version="4.1.0",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",