Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8c6e9e193 | ||
|
|
19adcab4a9 | ||
|
|
d2d2106246 | ||
|
|
9072faf7cf | ||
|
|
6f43b959bd | ||
|
|
1048feff52 | ||
|
|
7ff0a9650f | ||
|
|
8d5de5c49f | ||
|
|
96afdf8d83 | ||
|
|
aba0598947 | ||
|
|
142de460b9 | ||
|
|
2600209724 | ||
|
|
ed8a717bc5 | ||
|
|
0b7046b10d | ||
|
|
e784e717ca | ||
|
|
2895294903 | ||
|
|
8368ffa2fd | ||
|
|
23428c8ad2 | ||
|
|
c00b8c2cfe | ||
|
|
32003e59f9 | ||
|
|
bd32642b5c | ||
|
|
f463804087 | ||
|
|
b92b012309 | ||
|
|
74c295837c | ||
|
|
a543f5cb0c | ||
|
|
50c90a4328 | ||
|
|
374a88266a | ||
|
|
60cbc94a62 | ||
|
|
649305c83e | ||
|
|
b3397f4f4a | ||
|
|
a23604261a | ||
|
|
d9571b30b5 | ||
|
|
76b43f5d1e | ||
|
|
becfaaa054 | ||
|
|
bec301eb9d | ||
|
|
f28623b378 | ||
|
|
16837047be | ||
|
|
987c7ef34c | ||
|
|
dbe331afb8 | ||
|
|
86c0c55896 |
@@ -50,6 +50,9 @@ python3 manage.py collectstatic --no-input
|
||||
|
||||
| netbox version | netbox-topology-views version |
|
||||
| -------------- | ----------------------------- |
|
||||
| >= 4.0.0 | >= v4.0.0 |
|
||||
| >= 3.7.0 | >= v3.9.0 |
|
||||
| >= 3.6.4 | >= v3.8.1 |
|
||||
| >= 3.6.0 | >= v3.7.X |
|
||||
| >= 3.5.0 | >= v3.4.X |
|
||||
| >= 3.4.0 | >= v3.X.X |
|
||||
@@ -200,3 +203,12 @@ Set `Coordinate Groups` according to your needs:
|
||||
|
||||
Set `Coordinates` according to your needs:
|
||||
+ netbox_topology_views | coordinate | view/add/change/delete
|
||||
|
||||
Set `Power Feed Coordinates` according to your needs:
|
||||
+ netbox_topology_views | power feed coordinate | view/add/change/delete
|
||||
|
||||
Set `Power Panel Coordinates` according to your needs:
|
||||
+ netbox_topology_views | power panel coordinate | view/add/change/delete
|
||||
|
||||
Set `Circuit Coordinates` according to your needs:
|
||||
+ netbox_topology_views | circuit coordinate | view/add/change/delete
|
||||
|
||||
@@ -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.7.0"
|
||||
version = "4.0.1"
|
||||
author = "Mattijs Vanhaverbeke"
|
||||
author_email = "author@example.com"
|
||||
base_url = "netbox_topology_views"
|
||||
|
||||
@@ -2,7 +2,7 @@ from dcim.models import Device, DeviceRole
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from netbox.api.serializers import NetBoxModelSerializer
|
||||
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
|
||||
class TopologyDummySerializer(ModelSerializer):
|
||||
@@ -32,7 +32,22 @@ class CoordinateSerializer(NetBoxModelSerializer):
|
||||
model = Coordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
class CircuitCoordinateSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
class PowerPanelCoordinateSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ("x", "y")
|
||||
|
||||
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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +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
|
||||
@@ -14,12 +18,13 @@ from netbox_topology_views.api.serializers import (
|
||||
RoleImageSerializer,
|
||||
TopologyDummySerializer,
|
||||
)
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate
|
||||
import netbox_topology_views.models
|
||||
from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
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()
|
||||
@@ -41,20 +46,26 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
if device_id.startswith("c"):
|
||||
device_id = device_id.lstrip("c")
|
||||
actual_device = Circuit.objects.get(id=device_id)
|
||||
model_name = 'CircuitCoordinate'
|
||||
elif device_id.startswith("p"):
|
||||
device_id = device_id.lstrip("p")
|
||||
actual_device = PowerPanel.objects.get(id=device_id)
|
||||
model_name = 'PowerPanelCoordinate'
|
||||
elif device_id.startswith("f"):
|
||||
device_id = device_id.lstrip("f")
|
||||
actual_device = PowerFeed.objects.get(id=device_id)
|
||||
model_name = 'PowerFeedCoordinate'
|
||||
elif device_id.isnumeric():
|
||||
actual_device = Device.objects.get(id=device_id)
|
||||
model_name = 'Coordinate'
|
||||
|
||||
if not actual_device:
|
||||
return Response({"status": "invalid node_id in body"}, status=400)
|
||||
|
||||
model_class = getattr(netbox_topology_views.models, model_name)
|
||||
|
||||
if group_id is None or group_id == "default":
|
||||
group_id = Coordinate.get_or_create_default_group(group_id)
|
||||
group_id = model_class.get_or_create_default_group(group_id)
|
||||
if not group_id:
|
||||
return Response(
|
||||
{"status": "Error while creating default group."}, status=500
|
||||
@@ -66,12 +77,12 @@ class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
# Hen-and-egg-problem. Thanks, Django! By default, Django updates records that
|
||||
# already exist and inserts otherwise. This does not work with our
|
||||
# unique_together key if no pk is given. But: No record, no pk.
|
||||
if not Coordinate.objects.filter(group=group, device=actual_device):
|
||||
if not model_class.objects.filter(group=group, device=actual_device):
|
||||
# Unique group/device pair does not exist. Prepare new data set
|
||||
coords = Coordinate(group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
coords = model_class(group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
else:
|
||||
# Unique group/device pair already exists. Update data
|
||||
coords = Coordinate(pk=Coordinate.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
coords = model_class(pk=model_class.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord)
|
||||
coords.save()
|
||||
except:
|
||||
return Response(
|
||||
@@ -80,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
|
||||
@@ -100,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,
|
||||
@@ -117,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')
|
||||
|
||||
@@ -127,12 +173,12 @@ 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 = (
|
||||
"dcim.add_device_role",
|
||||
"dcim.change_device_role",
|
||||
"dcim.add_devicerole",
|
||||
"dcim.change_devicerole",
|
||||
)
|
||||
|
||||
@action(detail=False, methods=["post"])
|
||||
@@ -142,15 +188,26 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
||||
{"status": "Missing or malformed request body"}, status=400
|
||||
)
|
||||
|
||||
device_roles = {
|
||||
k: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.isnumeric()}
|
||||
content_type_ids = {
|
||||
k[2:]: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.startswith("ct") and k[2:].isnumeric()
|
||||
}
|
||||
if sys.version_info >= (3,9,0):
|
||||
device_roles = {
|
||||
k: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.isnumeric()}
|
||||
content_type_ids = {
|
||||
k[2:]: v.removeprefix(settings.STATIC_URL)
|
||||
for k, v in request.data.items()
|
||||
if k.startswith("ct") and k[2:].isnumeric()
|
||||
}
|
||||
else:
|
||||
device_roles = {}
|
||||
for k, v in request.data.items():
|
||||
if k.isdigit() and v.startswith(settings.STATIC_URL):
|
||||
device_roles[k] = v[len(settings.STATIC_URL):]
|
||||
|
||||
content_type_ids = {}
|
||||
for k, v in request.data.items():
|
||||
if k.startswith("ct") and k[2:].isdigit() and v.startswith(settings.STATIC_URL):
|
||||
content_type_ids[k[2:]] = v[len(settings.STATIC_URL):]
|
||||
|
||||
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
|
||||
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
|
||||
|
||||
@@ -1,28 +1,56 @@
|
||||
import django_filters
|
||||
from circuits.models import Circuit
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site
|
||||
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site, SiteGroup, Manufacturer, DeviceType, Platform, PowerPanel, PowerFeed
|
||||
from django.db.models import Q
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
from extras.models import ConfigTemplate
|
||||
from netbox.filtersets import NetBoxModelFilterSet
|
||||
from tenancy.filtersets import TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate
|
||||
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter, MultiValueCharFilter, MultiValueMACAddressFilter
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method="search",
|
||||
label="Search",
|
||||
)
|
||||
manufacturer_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
label="Manufacturer (ID)",
|
||||
)
|
||||
manufacturer = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device_type__manufacturer__slug',
|
||||
queryset=Manufacturer.objects.all(),
|
||||
to_field_name='slug',
|
||||
label="Manufacturer (slug)",
|
||||
)
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
label="Device type (ID)",
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name="role_id",
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label="Role (ID)",
|
||||
)
|
||||
platform_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Platform.objects.all(),
|
||||
label="Platform (ID)",
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name="site__region",
|
||||
lookup_expr="in",
|
||||
label="Region (ID)",
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name="site__group",
|
||||
lookup_expr="in",
|
||||
label="Site Group (ID)",
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label="Site (ID)",
|
||||
@@ -42,10 +70,57 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
choices=DeviceStatusChoices,
|
||||
null_value=None,
|
||||
)
|
||||
mac_address = MultiValueMACAddressFilter(
|
||||
field_name='interfaces__mac_address',
|
||||
label="MAC address",
|
||||
)
|
||||
serial = MultiValueCharFilter(
|
||||
lookup_expr='iexact'
|
||||
)
|
||||
console_ports = django_filters.BooleanFilter(
|
||||
method='_console_ports',
|
||||
label="Has console ports",
|
||||
)
|
||||
console_server_ports = django_filters.BooleanFilter(
|
||||
method='_console_server_ports',
|
||||
label="Has console server ports",
|
||||
)
|
||||
power_ports = django_filters.BooleanFilter(
|
||||
method='_power_ports',
|
||||
label="Has power ports",
|
||||
)
|
||||
power_outlets = django_filters.BooleanFilter(
|
||||
method='_power_outlets',
|
||||
label="Has power outlets",
|
||||
)
|
||||
interfaces = django_filters.BooleanFilter(
|
||||
method='_interfaces',
|
||||
label="Has interfaces",
|
||||
)
|
||||
pass_through_ports = django_filters.BooleanFilter(
|
||||
method='_pass_through_ports',
|
||||
label="Has pass-through ports",
|
||||
)
|
||||
config_template_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
label="Config template (ID)",
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
method='_has_primary_ip',
|
||||
label="Has a primary IP",
|
||||
)
|
||||
has_oob_ip = django_filters.BooleanFilter(
|
||||
method='_has_oob_ip',
|
||||
label="Has an out-of-band IP",
|
||||
)
|
||||
virtual_chassis_member = django_filters.BooleanFilter(
|
||||
method='_virtual_chassis_member',
|
||||
label="Is a virtual chassis member",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ["id", "name"]
|
||||
fields = ["id", "name", "asset_tag", "airflow"]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
@@ -54,6 +129,108 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
qs_filter = Q(name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def _console_ports(self, queryset, name, value):
|
||||
return queryset.exclude(consoleports__isnull=value)
|
||||
|
||||
def _console_server_ports(self, queryset, name, value):
|
||||
return queryset.exclude(consoleserverports__isnull=value)
|
||||
|
||||
def _power_ports(self, queryset, name, value):
|
||||
return queryset.exclude(powerports__isnull=value)
|
||||
|
||||
def _power_outlets(self, queryset, name, value):
|
||||
return queryset.exclude(poweroutlets__isnull=value)
|
||||
|
||||
def _interfaces(self, queryset, name, value):
|
||||
return queryset.exclude(interfaces__isnull=value)
|
||||
|
||||
def _pass_through_ports(self, queryset, name, value):
|
||||
return queryset.exclude(
|
||||
frontports__isnull=value,
|
||||
rearports__isnull=value
|
||||
)
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||
if value:
|
||||
return queryset.filter(params)
|
||||
return queryset.exclude(params)
|
||||
|
||||
def _has_oob_ip(self, queryset, name, value):
|
||||
params = Q(oob_ip__isnull=False)
|
||||
if value:
|
||||
return queryset.filter(params)
|
||||
return queryset.exclude(params)
|
||||
|
||||
def _virtual_chassis_member(self, queryset, name, value):
|
||||
return queryset.exclude(virtual_chassis__isnull=value)
|
||||
|
||||
class CircuitCoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
)
|
||||
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = Circuit.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ['id', 'group', 'device', 'x', 'y']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(group__name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
|
||||
class PowerPanelCoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
)
|
||||
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = PowerPanel.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ['id', 'group', 'device', 'x', 'y']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(group__name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
|
||||
class PowerFeedCoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
)
|
||||
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = PowerFeed.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ['id', 'group', 'device', 'x', 'y']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(group__name__icontains=value) |
|
||||
Q(device__name__icontains=value)
|
||||
)
|
||||
|
||||
class CoordinatesFilterSet(NetBoxModelFilterSet):
|
||||
group = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset = CoordinateGroup.objects.all(),
|
||||
|
||||
+550
-193
@@ -1,157 +1,344 @@
|
||||
from cProfile import label
|
||||
import imp
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Device, Site, Region, DeviceRole, Location, Rack
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Device, Site, SiteGroup, Region, DeviceRole, Location, Rack, Manufacturer, DeviceType, Platform, PowerPanel, PowerFeed
|
||||
|
||||
from django import forms
|
||||
from dcim.choices import DeviceStatusChoices
|
||||
from dcim.choices import DeviceStatusChoices, DeviceAirflowChoices
|
||||
from tenancy.models import TenantGroup, Tenant
|
||||
from tenancy.forms import TenancyFilterForm
|
||||
from extras.models import ConfigTemplate
|
||||
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,
|
||||
DynamicModelMultipleChoiceField
|
||||
)
|
||||
|
||||
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate
|
||||
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
class DeviceFilterForm(
|
||||
LocalConfigContextFilterForm,
|
||||
TenancyFilterForm,
|
||||
ContactModelFilterForm,
|
||||
NetBoxModelFilterSetForm
|
||||
):
|
||||
model = Device
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"q",
|
||||
"filter_id",
|
||||
),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'group', 'ignore_cable_type', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
||||
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
|
||||
'group_sites', 'group_locations', 'group_racks', 'group_virtualchassis', 'straight_cables', 'grid_size', name=_("Options")
|
||||
),
|
||||
(
|
||||
None,
|
||||
(
|
||||
"group",
|
||||
"save_coords",
|
||||
"show_unconnected",
|
||||
"show_cables",
|
||||
"show_logical_connections",
|
||||
"show_single_cable_logical_conns",
|
||||
"show_neighbors",
|
||||
"show_circuit",
|
||||
"show_power",
|
||||
"show_wireless",
|
||||
),
|
||||
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")
|
||||
),
|
||||
(
|
||||
None,
|
||||
(
|
||||
"tenant_group_id",
|
||||
"tenant_id",
|
||||
),
|
||||
FieldSet(
|
||||
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data', name=_("Miscellaneous")
|
||||
),
|
||||
(None, ("region_id", "site_id", "location_id", "rack_id")),
|
||||
(
|
||||
None,
|
||||
(
|
||||
"role_id",
|
||||
"id",
|
||||
"status",
|
||||
),
|
||||
),
|
||||
(None, ("tag",)),
|
||||
)
|
||||
group = forms.ModelChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False,
|
||||
label=_("Coordinate group"),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(), required=False, label=_("Region")
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(), required=False, label=_("Device Role")
|
||||
label=_('Coordinate group'),
|
||||
)
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
label=_("Device"),
|
||||
label=_('Device'),
|
||||
query_params={
|
||||
"location_id": "$location_id",
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
"role_id": "$role_id",
|
||||
'location_id': '$location_id',
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
'site_id': '$site_id',
|
||||
'role_id': '$role_id',
|
||||
'contact': '$contact',
|
||||
'contact_role': '$contact_role',
|
||||
'contact_group': '$contact_group',
|
||||
},
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site Group'),
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
'region_id': '$region_id',
|
||||
'group_id': '$site_group_id',
|
||||
},
|
||||
label=_("Site"),
|
||||
label=_('Site'),
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
'site_id': '$site_id',
|
||||
},
|
||||
label=_("Location"),
|
||||
label=_('Location'),
|
||||
)
|
||||
rack_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
"location_id": "$location_id",
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
'site_id': '$site_id',
|
||||
'location_id': '$location_id',
|
||||
},
|
||||
label=_("Rack"),
|
||||
label=_('Rack'),
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices, required=False, label=_("Device Status")
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
label=_('Device Status')
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
label=_('Role')
|
||||
)
|
||||
airflow = forms.MultipleChoiceField(
|
||||
label=_('Airflow'),
|
||||
choices=add_blank_choice(DeviceAirflowChoices),
|
||||
required=False
|
||||
)
|
||||
serial = forms.CharField(
|
||||
label=_('Serial'),
|
||||
required=False
|
||||
)
|
||||
asset_tag = forms.CharField(
|
||||
label=_('Asset tag'),
|
||||
required=False
|
||||
)
|
||||
mac_address = forms.CharField(
|
||||
required=False,
|
||||
label=_('MAC address')
|
||||
)
|
||||
manufacturer_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label=_('Manufacturer')
|
||||
)
|
||||
device_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceType.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'manufacturer_id': '$manufacturer_id'
|
||||
},
|
||||
label=_('Model')
|
||||
)
|
||||
platform_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Platform.objects.all(),
|
||||
required=False,
|
||||
null_option='None',
|
||||
label=_('Platform')
|
||||
)
|
||||
console_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has console ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
console_server_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has console server ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
power_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has power ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
power_outlets = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has power outlets'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
interfaces = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has interfaces'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
pass_through_ports = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has pass-through ports'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
config_template_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ConfigTemplate.objects.all(),
|
||||
required=False,
|
||||
label=_('Config template')
|
||||
)
|
||||
has_primary_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has a primary IP'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
has_oob_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Has an OOB IP'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
virtual_chassis_member = forms.NullBooleanField(
|
||||
required=False,
|
||||
label=_('Virtual chassis member'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
|
||||
tag = TagFilterField(model)
|
||||
|
||||
# options
|
||||
save_coords = forms.BooleanField(
|
||||
label=_("Save Coordinates"),
|
||||
ignore_cable_type = forms.MultipleChoiceField(
|
||||
label=_('Ignore Termination Types'),
|
||||
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"])
|
||||
choices=IndividualOptions.CHOICES
|
||||
)
|
||||
show_unconnected = forms.BooleanField(
|
||||
label=_("Show Unconnected"), required=False, initial=False
|
||||
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']),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_cables = forms.BooleanField(
|
||||
label =_("Show Cables"), required=False, initial=False
|
||||
show_unconnected = forms.NullBooleanField(
|
||||
label=_('Show Unconnected'),
|
||||
required=False,
|
||||
initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_logical_connections = forms.BooleanField(
|
||||
label =_("Show Logical Connections"), required=False, initial=False
|
||||
|
||||
show_cables = forms.NullBooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_('Show Cables'),
|
||||
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_logical_connections = forms.NullBooleanField(
|
||||
label =_('Show Logical Connections'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_neighbors = forms.BooleanField(
|
||||
label =_("Show Neighbors"), 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_circuit = forms.BooleanField(
|
||||
label=_("Show Circuit Terminations"), required=False, initial=False
|
||||
show_neighbors = forms.NullBooleanField(
|
||||
label =_('Show Neighbors'), 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_circuit = forms.NullBooleanField(
|
||||
label=_('Show Circuit Terminations'), 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_power = forms.NullBooleanField(
|
||||
label=_('Show Power Feeds'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
show_wireless = forms.NullBooleanField(
|
||||
label =_('Show Wireless Links'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
)
|
||||
group_sites = forms.NullBooleanField(
|
||||
label =_('Group Sites'), required=False, initial=False,
|
||||
widget=forms.Select(
|
||||
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:
|
||||
@@ -163,25 +350,142 @@ class CoordinateGroupsImportForm(NetBoxModelImportForm):
|
||||
model = CoordinateGroup
|
||||
fields = ('name', 'description')
|
||||
|
||||
class CircuitCoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Circuit Coordinate")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerPanelCoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Power Panel")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerFeedCoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("PowerFeed Coordinate")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CoordinatesForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
('Coordinate', ('group', 'device', 'x', 'y')),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Coordinate")),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Coordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CircuitCoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = CircuitCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerPanelCoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = PowerPanelCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class PowerFeedCoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = PowerFeedCoordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CoordinatesImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = Coordinate
|
||||
fields = ('group', 'device', 'x', 'y')
|
||||
|
||||
class CircuitCoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitCoordinate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("Circuit Coordinates")),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
x = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
y = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class PowerPanelCoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = PowerPanelCoordinate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_('PowerPanel Coordinates')),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerPanel.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
x = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
y = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
class PowerFeedCoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
model = Coordinate
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id'),
|
||||
FieldSet('group', 'device', 'x', 'y', name=_("PowerFeed Coordinates")),
|
||||
)
|
||||
|
||||
group = forms.ModelMultipleChoiceField(
|
||||
queryset=CoordinateGroup.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
device = DynamicModelMultipleChoiceField(
|
||||
queryset=PowerFeed.objects.all(),
|
||||
required=False
|
||||
)
|
||||
|
||||
x = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
y = forms.IntegerField(
|
||||
required=False
|
||||
)
|
||||
|
||||
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(
|
||||
@@ -204,133 +508,186 @@ class CoordinatesFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
class IndividualOptionsForm(NetBoxModelForm):
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
FieldSet
|
||||
(
|
||||
"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',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
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 "
|
||||
"cable is not displayed.")
|
||||
help_text=_('Choose Termination Types that you want to be ignored. '
|
||||
'If any ignored Termination Type is part of a connection, the '
|
||||
'cable is not displayed.')
|
||||
)
|
||||
preselected_device_roles = DynamicModelMultipleChoiceField(
|
||||
label=_("Preselected Device Role"),
|
||||
label=_('Preselected Device Role'),
|
||||
queryset=DeviceRole.objects.all(),
|
||||
required=False,
|
||||
help_text=_("Select Device Roles that you want to have "
|
||||
"preselected in the filter tab.")
|
||||
help_text=_('Select Device Roles that you want to have '
|
||||
'preselected in the filter tab.')
|
||||
)
|
||||
preselected_tags = forms.ModelMultipleChoiceField(
|
||||
label=_("Preselected Tags"),
|
||||
label=_('Preselected Tags'),
|
||||
queryset=Device.tags.all(),
|
||||
required=False,
|
||||
help_text=_("Select Tags that you want to have "
|
||||
"preselected in the filter tab.")
|
||||
help_text=_('Select Tags that you want to have '
|
||||
'preselected in the filter tab.')
|
||||
)
|
||||
save_coords = forms.BooleanField(
|
||||
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. "
|
||||
"It has no effect if 'allow_coordinates_saving' has not been set or "
|
||||
" 'always_save_coordinates' has been set.")
|
||||
)
|
||||
show_unconnected = forms.BooleanField(
|
||||
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,
|
||||
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,
|
||||
initial=False,
|
||||
help_text=_("Displays connections between devices that are not "
|
||||
"directly connected (e.g. via patch panels). These connections "
|
||||
"are displayed as yellow dotted lines.")
|
||||
)
|
||||
show_single_cable_logical_conns = forms.BooleanField(
|
||||
label = ("Show redundant Cable and Logical Connection"),
|
||||
required = False,
|
||||
initial=False,
|
||||
help_text=_("Shows a logical connection (in addition to a cable), "
|
||||
"even if a cable is directly connected. Leaving this option "
|
||||
"disabled prevents that redundant display. This option only "
|
||||
"has an effect if 'Show Logical Connections' is activated.")
|
||||
)
|
||||
show_neighbors = forms.BooleanField(
|
||||
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,
|
||||
initial=False,
|
||||
help_text=_("Displays connections between circuit terminations. "
|
||||
"These connections are displayed as blue dashed lines.")
|
||||
)
|
||||
show_power = forms.BooleanField(
|
||||
label=_("Show Power Feeds"),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_("Displays connections between power outlets and power "
|
||||
"ports. These connections are displayed as solid lines in the "
|
||||
"color of the cable. This option depends on 'Show Cables'.")
|
||||
)
|
||||
show_wireless = forms.BooleanField(
|
||||
label =_("Show Wireless Links"),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_("Displays wireless connections. These connections are "
|
||||
"displayed as blue dotted lines.")
|
||||
)
|
||||
draw_default_layout = forms.BooleanField(
|
||||
label = ("Draw Default Layout"),
|
||||
label=_('Save Coordinates'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_("Enable this option if you want to draw the topology on "
|
||||
"the initial load (when you go to the topology plugin page).")
|
||||
help_text=_('Coordinates of nodes will be saved if dragged to a different '
|
||||
'position. This option depends on parameters set in the config file. '
|
||||
'It has no effect if \'allow_coordinates_saving\' has not been set or '
|
||||
' \'always_save_coordinates\' has been set.')
|
||||
)
|
||||
show_unconnected = forms.BooleanField(
|
||||
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,
|
||||
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,
|
||||
initial=False,
|
||||
help_text=_('Displays connections between devices that are not '
|
||||
'directly connected (e.g. via patch panels). These connections '
|
||||
'are displayed as yellow dotted lines.')
|
||||
)
|
||||
show_single_cable_logical_conns = forms.BooleanField(
|
||||
label = ('Show redundant Cable and Logical Connection'),
|
||||
required = False,
|
||||
initial=False,
|
||||
help_text=_('Shows a logical connection (in addition to a cable), '
|
||||
'even if a cable is directly connected. Leaving this option '
|
||||
'disabled prevents that redundant display. This option only '
|
||||
'has an effect if \'Show Logical Connections\' is activated.')
|
||||
)
|
||||
show_neighbors = forms.BooleanField(
|
||||
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,
|
||||
initial=False,
|
||||
help_text=_('Displays connections between circuit terminations. '
|
||||
'These connections are displayed as blue dashed lines.')
|
||||
)
|
||||
show_power = forms.BooleanField(
|
||||
label=_('Show Power Feeds'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Displays connections between power outlets and power '
|
||||
'ports. These connections are displayed as solid lines in the '
|
||||
'color of the cable. This option depends on \'Show Cables\'.')
|
||||
)
|
||||
show_wireless = forms.BooleanField(
|
||||
label =_('Show Wireless Links'),
|
||||
required=False,
|
||||
initial=False,
|
||||
help_text=_('Displays wireless connections. These connections are '
|
||||
'displayed as blue dotted lines.')
|
||||
)
|
||||
group_sites = forms.BooleanField(
|
||||
label =_('Group Sites'),
|
||||
required=False,
|
||||
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,
|
||||
initial=False,
|
||||
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'
|
||||
]
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 4.1.8 on 2023-09-25 21:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('netbox_topology_views', '0005_individualoptions_save_coords'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PowerPanelCoordinate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('x', models.IntegerField()),
|
||||
('y', models.IntegerField()),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.powerpanel')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group', 'device'],
|
||||
'unique_together': {('device', 'group')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PowerFeedCoordinate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('x', models.IntegerField()),
|
||||
('y', models.IntegerField()),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.powerfeed')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group', 'device'],
|
||||
'unique_together': {('device', 'group')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CircuitCoordinate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('x', models.IntegerField()),
|
||||
('y', models.IntegerField()),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='circuits.circuit')),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['group', 'device'],
|
||||
'unique_together': {('device', 'group')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
+191
-11
@@ -1,7 +1,8 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from dcim.models import Device, DeviceRole
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Device, DeviceRole, PowerPanel, PowerFeed
|
||||
from extras.models import Tag
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -12,7 +13,7 @@ from netbox.models import NetBoxModel
|
||||
from netbox.models.features import (
|
||||
ChangeLoggingMixin,
|
||||
ExportTemplatesMixin,
|
||||
WebhooksMixin,
|
||||
EventRulesMixin,
|
||||
)
|
||||
|
||||
from netbox_topology_views.utils import (
|
||||
@@ -25,7 +26,7 @@ from netbox_topology_views.utils import (
|
||||
)
|
||||
|
||||
|
||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
@@ -40,6 +41,8 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
||||
|
||||
__role: Optional[Role] = None
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
@property
|
||||
def role(self) -> Role:
|
||||
if self.__role:
|
||||
@@ -110,6 +113,8 @@ class CoordinateGroup(NetBoxModel):
|
||||
blank = True,
|
||||
)
|
||||
|
||||
_netbox_private = True
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
@@ -130,13 +135,15 @@ class Coordinate(NetBoxModel):
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further up on the monitor.',
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
'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
|
||||
@@ -168,15 +175,168 @@ class Coordinate(NetBoxModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk])
|
||||
|
||||
class CircuitCoordinate(NetBoxModel):
|
||||
"""
|
||||
Coordinates are being used to place devices in a topology view onto a certain
|
||||
position. Devices belong to one or more coordinate groups. They have to
|
||||
be unique together.
|
||||
"""
|
||||
device = models.ForeignKey(Circuit, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'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
|
||||
# selected. The default group will be added automatically if it does not exist.
|
||||
try:
|
||||
if CoordinateGroup.objects.filter(name="default"):
|
||||
group = CoordinateGroup.objects.get(name="default")
|
||||
group_id = group.pk
|
||||
else:
|
||||
group = CoordinateGroup(
|
||||
name="default",
|
||||
description="Automatically generated default group. If you delete "
|
||||
"this group, all default coordinates are gone for good but "
|
||||
"the group itself will be re-created."
|
||||
)
|
||||
group.save()
|
||||
group_id = group.pk
|
||||
except:
|
||||
return False
|
||||
return group_id
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'device']
|
||||
unique_together = ('device', 'group')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.x};{self.y}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:circuitcoordinate', args=[self.pk])
|
||||
|
||||
class PowerPanelCoordinate(NetBoxModel):
|
||||
"""
|
||||
Coordinates are being used to place devices in a topology view onto a certain
|
||||
position. Devices belong to one or more coordinate groups. They have to
|
||||
be unique together.
|
||||
"""
|
||||
device = models.ForeignKey(PowerPanel, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'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
|
||||
# selected. The default group will be added automatically if it does not exist.
|
||||
try:
|
||||
if CoordinateGroup.objects.filter(name="default"):
|
||||
group = CoordinateGroup.objects.get(name="default")
|
||||
group_id = group.pk
|
||||
else:
|
||||
group = CoordinateGroup(
|
||||
name="default",
|
||||
description="Automatically generated default group. If you delete "
|
||||
"this group, all default coordinates are gone for good but "
|
||||
"the group itself will be re-created."
|
||||
)
|
||||
group.save()
|
||||
group_id = group.pk
|
||||
except:
|
||||
return False
|
||||
return group_id
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'device']
|
||||
unique_together = ('device', 'group')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.x};{self.y}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:powerpanelcoordinate', args=[self.pk])
|
||||
|
||||
class PowerFeedCoordinate(NetBoxModel):
|
||||
"""
|
||||
Coordinates are being used to place devices in a topology view onto a certain
|
||||
position. Devices belong to one or more coordinate groups. They have to
|
||||
be unique together.
|
||||
"""
|
||||
device = models.ForeignKey(PowerFeed, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
|
||||
|
||||
x = models.IntegerField(
|
||||
help_text='X-coordinate of the device (horizontal) on the canvas. '
|
||||
'Smaller values correspond to a position further to the left on the monitor.',
|
||||
)
|
||||
y = models.IntegerField(
|
||||
help_text='Y-coordinate of the device (vertical) on the canvas. '
|
||||
'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
|
||||
# selected. The default group will be added automatically if it does not exist.
|
||||
try:
|
||||
if CoordinateGroup.objects.filter(name="default"):
|
||||
group = CoordinateGroup.objects.get(name="default")
|
||||
group_id = group.pk
|
||||
else:
|
||||
group = CoordinateGroup(
|
||||
name="default",
|
||||
description="Automatically generated default group. If you delete "
|
||||
"this group, all default coordinates are gone for good but "
|
||||
"the group itself will be re-created."
|
||||
)
|
||||
group.save()
|
||||
group_id = group.pk
|
||||
except:
|
||||
return False
|
||||
return group_id
|
||||
|
||||
class Meta:
|
||||
ordering = ['group', 'device']
|
||||
unique_together = ('device', 'group')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.x};{self.y}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('plugins:netbox_topology_views:powerfeedcoordinate', args=[self.pk])
|
||||
|
||||
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(
|
||||
@@ -226,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,36 +1,76 @@
|
||||
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']
|
||||
)
|
||||
)
|
||||
|
||||
circuitcoordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:circuitcoordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:circuitcoordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
powerpanelcoordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerpanelcoordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerpanelcoordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
powerfeedcoordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerfeedcoordinate_add',
|
||||
title='Add',
|
||||
icon_class='mdi mdi-plus-thick',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
),
|
||||
PluginMenuButton(
|
||||
link='plugins:netbox_topology_views:powerfeedcoordinate_import',
|
||||
title='Import',
|
||||
icon_class='mdi mdi-upload',
|
||||
permissions=['netbox_topology_views.add_coordinate']
|
||||
)
|
||||
)
|
||||
|
||||
coordinate_buttons = (
|
||||
PluginMenuButton(
|
||||
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']
|
||||
)
|
||||
)
|
||||
@@ -42,13 +82,20 @@ menu = PluginMenu(
|
||||
('TOPOLOGY',
|
||||
(
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology", permissions=["dcim.view_site", "dcim.view_device"]),
|
||||
),
|
||||
),
|
||||
('COORDINATES',
|
||||
(
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinategroup_list", link_text="Coordinate Groups", buttons=coordinategroup_buttons, permissions=['netbox_topology_views.view_coordinategroup']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Coordinates", buttons=coordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Device Coordinates", buttons=coordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:powerfeedcoordinate_list", link_text="Power Feed Coords", buttons=powerfeedcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:powerpanelcoordinate_list", link_text="Power Panel Coords", buttons=powerpanelcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:circuitcoordinate_list", link_text="Circuit Coordinates", buttons=circuitcoordinate_buttons, permissions=['netbox_topology_views.view_coordinate']),
|
||||
),
|
||||
),
|
||||
('PREFERENCES',
|
||||
(
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images", permissions=[ "dcim.view_site","dcim.view_device_role"]),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images", permissions=[ "dcim.view_site","dcim.view_devicerole"]),
|
||||
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options", permissions=['netbox_topology_views.change_individualoptions']),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1 +1 @@
|
||||
#visgraph{height:64vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
|
||||
#visgraph{height:64vh;border:1px solid #ced4da}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529;border:1px solid #495057}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,13 @@
|
||||
#visgraph {
|
||||
height: 64vh;
|
||||
border: 1px solid #ced4da
|
||||
}
|
||||
|
||||
html[data-netbox-color-mode=dark] #visgraph {
|
||||
background-color: #212529;
|
||||
border: 1px solid #495057
|
||||
}
|
||||
|
||||
|
||||
.image-dropdown img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
@@ -69,17 +69,160 @@ 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(
|
||||
async ([nodeId, nodePosition]) => {
|
||||
window.nodes.update({id: parseInt(nodeId), physics: false, x: nodePosition.x, y: nodePosition.y});
|
||||
if(!isNaN(parseInt(nodeId))) {
|
||||
nodeKey = parseInt(nodeId);
|
||||
}
|
||||
else {
|
||||
nodeKey = nodeId;
|
||||
}
|
||||
|
||||
try {
|
||||
window.nodes.update({id: nodeKey, physics: false, x: nodePosition.x, y: nodePosition.y});
|
||||
}
|
||||
catch (e) {
|
||||
console.log([
|
||||
'Error while executing window.nodes.update()',
|
||||
'nodeId: ' + nodeId,
|
||||
'nodeKey: ' + nodeKey,
|
||||
'x: ' + nodePosition.x,
|
||||
'y: ' + nodePosition.y
|
||||
]);
|
||||
console.log(e);
|
||||
}
|
||||
const res = await fetch(
|
||||
'/' + basePath + 'api/plugins/netbox_topology_views/save-coords/save_coords/',
|
||||
{
|
||||
@@ -97,8 +240,6 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
console.log(nodeId, res.status, res.statusText)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -116,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
|
||||
@@ -191,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']
|
||||
})
|
||||
|
||||
+81
-74
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.6.2",
|
||||
"version": "4.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.6.2",
|
||||
"version": "4.0.1",
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.6",
|
||||
"vis-network": "^9.1.6",
|
||||
"vis-util": "^5.0.3"
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"vis-util": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
@@ -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.77.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
|
||||
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -716,7 +725,8 @@
|
||||
"node_modules/timsort": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
|
||||
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
@@ -739,10 +749,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vis-data": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
|
||||
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg==",
|
||||
"hasInstallScript": true,
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz",
|
||||
"integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
@@ -753,10 +762,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vis-network": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
|
||||
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==",
|
||||
"hasInstallScript": true,
|
||||
"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==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
@@ -765,16 +773,15 @@
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0",
|
||||
"keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||
"vis-data": "^6.3.0 || ^7.0.0",
|
||||
"vis-util": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vis-util": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
|
||||
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz",
|
||||
"integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -784,7 +791,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0"
|
||||
"component-emitter": "^1.3.0 || ^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.7.0",
|
||||
"version": "4.0.1",
|
||||
"scripts": {
|
||||
"bundle": "node bundle.js",
|
||||
"bundle:styles": "node bundle.js --styles",
|
||||
"bundle:scripts": "node bundle.js --scripts"
|
||||
},
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.6",
|
||||
"vis-network": "^9.1.6",
|
||||
"vis-util": "^5.0.3"
|
||||
"vis-data": "^7.1.9",
|
||||
"vis-network": "^9.1.9",
|
||||
"vis-util": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import django_tables2 as tables
|
||||
|
||||
from netbox.tables import NetBoxTable, ChoiceFieldColumn
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate
|
||||
from netbox_topology_views.models import CoordinateGroup, Coordinate, CircuitCoordinate, PowerPanelCoordinate, PowerFeedCoordinate
|
||||
|
||||
class CoordinateGroupListTable(NetBoxTable):
|
||||
name = tables.Column(
|
||||
@@ -14,6 +14,48 @@ class CoordinateGroupListTable(NetBoxTable):
|
||||
fields = ('pk', 'id', 'name', 'description', 'devices')
|
||||
default_columns = ('name', 'description', 'devices')
|
||||
|
||||
class CircuitCoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitCoordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
class PowerPanelCoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerPanelCoordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
class PowerFeedCoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = PowerFeedCoordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
class CoordinateListTable(NetBoxTable):
|
||||
group = tables.Column(
|
||||
linkify=True
|
||||
@@ -27,4 +69,3 @@ class CoordinateListTable(NetBoxTable):
|
||||
model = Coordinate
|
||||
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
|
||||
default_columns = ('id', 'group', 'device', 'x', 'y')
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
from extras.plugins import PluginTemplateExtension
|
||||
from netbox.plugins import PluginTemplateExtension
|
||||
from django.conf import settings
|
||||
from packaging import version
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Topology Views Coordinates{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Coordinates
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuit</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">X-Coordinate</th>
|
||||
<td>{{ object.x }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Y-Coordinate</th>
|
||||
<td>{{ object.y }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Add new Circuit Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Edit Circuit Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block title %}Circuit Coordinates{% endblock title %}
|
||||
@@ -32,10 +32,34 @@
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Coordinates</h5>
|
||||
<h5 class="card-header">Circuit Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table circuitcoordinates_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Power Panel Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table powerpanelcoordinates_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Power Feed Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table powerfeedcoordinates_table %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Device Coordinates</h5>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table coordinates_table %}
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
<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,13 +48,15 @@
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
<div class="tab-content">
|
||||
<div class="tab-content" id="tabcontentwrapper">
|
||||
|
||||
{# Applied filters #}
|
||||
{% if filter_form %}
|
||||
{% applied_filters model filter_form request.GET %}
|
||||
<div id="filterwrapper">
|
||||
{% applied_filters model filter_form request.GET %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||
@@ -71,13 +73,42 @@
|
||||
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock content-wrapper %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block javascript %}
|
||||
<script type="text/javascript">
|
||||
const brokenImage = '{{ broken_image }}';
|
||||
const topologyData = {{ topology_data | safe }};
|
||||
const basePath = '{{ basepath }}';
|
||||
</script>
|
||||
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
resizeCanvas();
|
||||
|
||||
function resizeCanvas() {
|
||||
heightToSubtract = getElementHeight('.page-header') +
|
||||
getElementHeight('#tabswrapper') +
|
||||
getElementHeight('#filterwrapper') +
|
||||
getElementHeight('.footer') +
|
||||
getElementPadding('#tabcontentwrapper');
|
||||
|
||||
document.getElementById("visgraph").style.height = "calc(100vh - " + heightToSubtract + "px)";
|
||||
}
|
||||
|
||||
function getElementPadding(element) {
|
||||
el = document.querySelector(element);
|
||||
els = getComputedStyle(el);
|
||||
padding = parseInt(els.paddingTop) + parseInt(els.paddingBottom);
|
||||
|
||||
return padding;
|
||||
}
|
||||
|
||||
function getElementHeight(element) {
|
||||
el = document.querySelector(element);
|
||||
els = getComputedStyle(el);
|
||||
height = el.offsetHeight + parseInt(els.marginTop) + parseInt(els.marginBottom);
|
||||
|
||||
return height;
|
||||
}
|
||||
</script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
|
||||
{% endblock javascript %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Topology Views Coordinates{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Coordinates
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Power Feed</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">X-Coordinate</th>
|
||||
<td>{{ object.x }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Y-Coordinate</th>
|
||||
<td>{{ object.y }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Add new Power Feed Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Edit Power Feed Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block title %}Power Feed Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
|
||||
{% block title %}Topology Views Coordinates{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
Coordinates
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Group</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Power Panel</th>
|
||||
<td>{{ object.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">X-Coordinate</th>
|
||||
<td>{{ object.x }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Y-Coordinate</th>
|
||||
<td>{{ object.y }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Add new Power Panel Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_edit.html' %}
|
||||
|
||||
{% block title %}Edit Power Panel Coordinates{% endblock title %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends 'generic/object_list.html' %}
|
||||
|
||||
{% block title %}Power Panel Coordinates{% endblock title %}
|
||||
@@ -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>
|
||||
|
||||
@@ -20,6 +20,33 @@ urlpatterns = (
|
||||
path("coordinate-groups/<int:pk>/delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroup_delete"),
|
||||
path("coordinate-groups/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="coordinategroup_changelog", kwargs={'model': models.CoordinateGroup}),
|
||||
|
||||
# Circuit Coordinate
|
||||
path("circuitcoordinate/", views.CircuitCoordinateListView.as_view(), name="circuitcoordinate_list"),
|
||||
path("circuitcoordinate/add/", views.CircuitCoordinateAddView.as_view(), name="circuitcoordinate_add"),
|
||||
path('circuitcoordinate/import/', views.CircuitCoordinateBulkImportView.as_view(), name='circuitcoordinate_import'),
|
||||
path("circuitcoordinate/<int:pk>/", views.CircuitCoordinateView.as_view(), name="circuitcoordinate"),
|
||||
path("circuitcoordinate/<int:pk>/edit/", views.CircuitCoordinateEditView.as_view(), name="circuitcoordinate_edit"),
|
||||
path("circuitcoordinate/<int:pk>/delete/", views.CircuitCoordinateDeleteView.as_view(), name="circuitcoordinate_delete"),
|
||||
path("circuitcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="circuitcoordinate_changelog", kwargs={'model': models.CircuitCoordinate}),
|
||||
|
||||
# Power Panel Coordinate
|
||||
path("powerpanelcoordinate/", views.PowerPanelCoordinateListView.as_view(), name="powerpanelcoordinate_list"),
|
||||
path("powerpanelcoordinate/add/", views.PowerPanelCoordinateAddView.as_view(), name="powerpanelcoordinate_add"),
|
||||
path('powerpanelcoordinate/import/', views.PowerPanelCoordinateBulkImportView.as_view(), name='powerpanelcoordinate_import'),
|
||||
path("powerpanelcoordinate/<int:pk>/", views.PowerPanelCoordinateView.as_view(), name="powerpanelcoordinate"),
|
||||
path("powerpanelcoordinate/<int:pk>/edit/", views.PowerPanelCoordinateEditView.as_view(), name="powerpanelcoordinate_edit"),
|
||||
path("powerpanelcoordinate/<int:pk>/delete/", views.PowerPanelCoordinateDeleteView.as_view(), name="powerpanelcoordinate_delete"),
|
||||
path("powerpanelcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="powerpanelcoordinate_changelog", kwargs={'model': models.PowerPanelCoordinate}),
|
||||
|
||||
# Power Feed Coordinate
|
||||
path("powerfeedcoordinate/", views.PowerFeedCoordinateListView.as_view(), name="powerfeedcoordinate_list"),
|
||||
path("powerfeedcoordinate/add/", views.PowerFeedCoordinateAddView.as_view(), name="powerfeedcoordinate_add"),
|
||||
path('powerfeedcoordinate/import/', views.PowerFeedCoordinateBulkImportView.as_view(), name='powerfeedcoordinate_import'),
|
||||
path("powerfeedcoordinate/<int:pk>/", views.PowerFeedCoordinateView.as_view(), name="powerfeedcoordinate"),
|
||||
path("powerfeedcoordinate/<int:pk>/edit/", views.PowerFeedCoordinateEditView.as_view(), name="powerfeedcoordinate_edit"),
|
||||
path("powerfeedcoordinate/<int:pk>/delete/", views.PowerFeedCoordinateDeleteView.as_view(), name="powerfeedcoordinate_delete"),
|
||||
path("powerfeedcoordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="powerfeedcoordinate_changelog", kwargs={'model': models.PowerFeedCoordinate}),
|
||||
|
||||
# Coordinate
|
||||
path("coordinate/", views.CoordinateListView.as_view(), name="coordinate_list"),
|
||||
path("coordinate/add/", views.CoordinateAddView.as_view(), name="coordinate_add"),
|
||||
|
||||
@@ -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]
|
||||
|
||||
+291
-41
@@ -4,8 +4,7 @@ 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
|
||||
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
|
||||
from dcim.models import (
|
||||
Cable,
|
||||
CableTermination,
|
||||
@@ -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,
|
||||
@@ -37,20 +37,36 @@ from netbox.views.generic import (
|
||||
ObjectChangeLogView,
|
||||
BulkImportView
|
||||
)
|
||||
|
||||
|
||||
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet
|
||||
from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet, CircuitCoordinatesFilterSet, PowerPanelCoordinatesFilterSet, PowerFeedCoordinatesFilterSet
|
||||
from netbox_topology_views.forms import (
|
||||
DeviceFilterForm,
|
||||
IndividualOptionsForm,
|
||||
CoordinateGroupsForm,
|
||||
CircuitCoordinatesForm,
|
||||
CircuitCoordinatesFilterForm,
|
||||
CircuitCoordinatesImportForm,
|
||||
PowerPanelCoordinatesForm,
|
||||
PowerPanelCoordinatesFilterForm,
|
||||
PowerPanelCoordinatesImportForm,
|
||||
PowerFeedCoordinatesForm,
|
||||
PowerFeedCoordinatesFilterForm,
|
||||
PowerFeedCoordinatesImportForm,
|
||||
CoordinatesForm,
|
||||
CoordinatesFilterForm,
|
||||
CoordinateGroupsImportForm,
|
||||
CoordinatesImportForm
|
||||
)
|
||||
from netbox_topology_views.models import RoleImage, CoordinateGroup, Coordinate, IndividualOptions
|
||||
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable
|
||||
import netbox_topology_views.models
|
||||
from netbox_topology_views.models import (
|
||||
RoleImage,
|
||||
IndividualOptions,
|
||||
CoordinateGroup,
|
||||
Coordinate,
|
||||
CircuitCoordinate,
|
||||
PowerPanelCoordinate,
|
||||
PowerFeedCoordinate,
|
||||
)
|
||||
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable, CircuitCoordinateListTable, PowerPanelCoordinateListTable, PowerFeedCoordinateListTable
|
||||
from netbox_topology_views.utils import (
|
||||
CONF_IMAGE_DIR,
|
||||
find_image_url,
|
||||
@@ -76,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__)
|
||||
)
|
||||
|
||||
|
||||
@@ -88,6 +104,7 @@ def create_node(
|
||||
if isinstance(device, Circuit):
|
||||
dev_name = device.cid
|
||||
node["id"] = f"c{device.pk}"
|
||||
model_name = 'CircuitCoordinate'
|
||||
|
||||
if device.provider is not None:
|
||||
node_content += (
|
||||
@@ -98,6 +115,7 @@ def create_node(
|
||||
elif isinstance(device, PowerPanel):
|
||||
dev_name = device.name
|
||||
node["id"] = f"p{device.pk}"
|
||||
model_name = 'PowerPanelCoordinate'
|
||||
|
||||
if device.site is not None:
|
||||
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
|
||||
@@ -108,6 +126,7 @@ def create_node(
|
||||
elif isinstance(device, PowerFeed):
|
||||
dev_name = device.name
|
||||
node["id"] = f"f{device.pk}"
|
||||
model_name = 'PowerFeedCoordinate'
|
||||
|
||||
if device.power_panel is not None:
|
||||
node_content += (
|
||||
@@ -124,6 +143,7 @@ def create_node(
|
||||
if device.voltage is not None:
|
||||
node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>"
|
||||
else:
|
||||
model_name = 'Coordinate'
|
||||
dev_name = device.name
|
||||
if dev_name is None:
|
||||
dev_name = device.device_type.get_full_name
|
||||
@@ -132,9 +152,9 @@ def create_node(
|
||||
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>"
|
||||
@@ -160,11 +180,26 @@ 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)
|
||||
|
||||
if group_id is None or group_id == "default":
|
||||
group_id = Coordinate.get_or_create_default_group(group_id)
|
||||
group_id = model_class.get_or_create_default_group(group_id)
|
||||
if not group_id:
|
||||
print('Exception occured while handling default group.')
|
||||
return node
|
||||
@@ -176,10 +211,10 @@ def create_node(
|
||||
# will not be placed correctly by vis-network.
|
||||
node["x"] = 0
|
||||
node["y"] = 0
|
||||
if Coordinate.objects.filter(group=group, device=device.pk).values('x') and Coordinate.objects.filter(group=group, device=device.pk).values('y'):
|
||||
if model_class.objects.filter(group=group, device=device.pk).values('x') and model_class.objects.filter(group=group, device=device.pk).values('y'):
|
||||
# Coordinates data for the device exists in Coordinates Group. Let's assign them
|
||||
node["x"] = Coordinate.objects.get(group=group, device=device.pk).x
|
||||
node["y"] = Coordinate.objects.get(group=group, device=device.pk).y
|
||||
node["x"] = model_class.objects.get(group=group, device=device.pk).x
|
||||
node["y"] = model_class.objects.get(group=group, device=device.pk).y
|
||||
node["physics"] = False
|
||||
elif "coordinates" in device.custom_field_data:
|
||||
# We prefer the new Coordinate model but leave the deprecated method
|
||||
@@ -206,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,
|
||||
@@ -268,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
|
||||
|
||||
|
||||
@@ -295,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,
|
||||
@@ -303,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 = []
|
||||
@@ -316,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] = {}
|
||||
@@ -324,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]
|
||||
|
||||
@@ -352,7 +397,8 @@ def get_topology_data(
|
||||
)
|
||||
for path_complete_interface in path_complete_interfaces:
|
||||
for connected_endpoint in path_complete_interface.connected_endpoints:
|
||||
device_ids.append(connected_endpoint.device.id)
|
||||
if type(connected_endpoint) != ProviderNetwork:
|
||||
device_ids.append(connected_endpoint.device.id)
|
||||
|
||||
if show_circuit:
|
||||
circuit_terminations = CircuitTermination.objects.filter(
|
||||
@@ -399,6 +445,7 @@ def get_topology_data(
|
||||
circuit=circuit_model,
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -468,6 +515,7 @@ def get_topology_data(
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
power=True,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -506,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
|
||||
|
||||
@@ -516,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:
|
||||
@@ -586,6 +634,7 @@ def get_topology_data(
|
||||
cable=link.cable,
|
||||
termination_a=termination_a,
|
||||
termination_b=termination_b,
|
||||
straight_cables=straight_cables,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -625,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
|
||||
@@ -640,6 +703,7 @@ def get_topology_data(
|
||||
results["nodes"] = nodes
|
||||
results["edges"] = edges
|
||||
results["group"] = group_id
|
||||
results["options"] = options
|
||||
return results
|
||||
|
||||
|
||||
@@ -652,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
|
||||
@@ -665,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"]
|
||||
|
||||
@@ -676,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,
|
||||
@@ -685,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",
|
||||
@@ -748,9 +857,9 @@ ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
|
||||
class TopologyImagesView(PermissionRequiredMixin, View):
|
||||
permission_required = (
|
||||
"dcim.view_site",
|
||||
"dcim.view_device_role",
|
||||
"dcim.add_device_role",
|
||||
"dcim.change_device_role",
|
||||
"dcim.view_devicerole",
|
||||
"dcim.add_devicerole",
|
||||
"dcim.change_devicerole",
|
||||
)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
@@ -799,6 +908,132 @@ class TopologyImagesView(PermissionRequiredMixin, View):
|
||||
},
|
||||
)
|
||||
|
||||
class CircuitCoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
|
||||
class CircuitCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.add_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
form = CircuitCoordinatesForm
|
||||
template_name = 'netbox_topology_views/circuitcoordinate_add.html'
|
||||
|
||||
class CircuitCoordinateBulkImportView(BulkImportView):
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
model_form = CircuitCoordinatesImportForm
|
||||
|
||||
class CircuitCoordinateListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
table = CircuitCoordinateListTable
|
||||
template_name = 'netbox_topology_views/circuitcoordinate_list.html'
|
||||
filterset = CircuitCoordinatesFilterSet
|
||||
filterset_form = CircuitCoordinatesFilterForm
|
||||
|
||||
class CircuitCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
form = CircuitCoordinatesForm
|
||||
template_name = 'netbox_topology_views/circuitcoordinate_edit.html'
|
||||
|
||||
class CircuitCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_topology_views.delete_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
|
||||
class CircuitCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = CircuitCoordinate.objects.all()
|
||||
|
||||
class PowerPanelCoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
|
||||
class PowerPanelCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.add_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
form = PowerPanelCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerpanelcoordinate_add.html'
|
||||
|
||||
class PowerPanelCoordinateBulkImportView(BulkImportView):
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
model_form = PowerPanelCoordinatesImportForm
|
||||
|
||||
class PowerPanelCoordinateListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
table = PowerPanelCoordinateListTable
|
||||
template_name = 'netbox_topology_views/powerpanelcoordinate_list.html'
|
||||
filterset = PowerPanelCoordinatesFilterSet
|
||||
filterset_form = PowerPanelCoordinatesFilterForm
|
||||
|
||||
class PowerPanelCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
form = PowerPanelCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerpanelcoordinate_edit.html'
|
||||
|
||||
class PowerPanelCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_topology_views.delete_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
|
||||
class PowerPanelCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerPanelCoordinate.objects.all()
|
||||
|
||||
class PowerFeedCoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
|
||||
class PowerFeedCoordinateAddView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.add_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
form = PowerFeedCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerfeedcoordinate_add.html'
|
||||
|
||||
class PowerFeedCoordinateBulkImportView(BulkImportView):
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
model_form = PowerFeedCoordinatesImportForm
|
||||
|
||||
class PowerFeedCoordinateListView(PermissionRequiredMixin, ObjectListView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
table = PowerFeedCoordinateListTable
|
||||
template_name = 'netbox_topology_views/powerfeedcoordinate_list.html'
|
||||
filterset = PowerFeedCoordinatesFilterSet
|
||||
filterset_form = PowerFeedCoordinatesFilterForm
|
||||
|
||||
class PowerFeedCoordinateEditView(PermissionRequiredMixin, ObjectEditView):
|
||||
permission_required = 'netbox_topology_views.change_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
form = PowerFeedCoordinatesForm
|
||||
template_name = 'netbox_topology_views/powerfeedcoordinate_edit.html'
|
||||
|
||||
class PowerFeedCoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
||||
permission_required = 'netbox_topology_views.delete_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
|
||||
class PowerFeedCoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
queryset = PowerFeedCoordinate.objects.all()
|
||||
|
||||
class CoordinateView(PermissionRequiredMixin, ObjectView):
|
||||
permission_required = 'netbox_topology_views.view_coordinate'
|
||||
|
||||
@@ -847,10 +1082,19 @@ class CoordinateGroupView(PermissionRequiredMixin, ObjectView):
|
||||
queryset = CoordinateGroup.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
circuittable = CircuitCoordinateListTable(instance.circuitcoordinate_set.all())
|
||||
circuittable.configure(request)
|
||||
powerpaneltable = PowerPanelCoordinateListTable(instance.powerpanelcoordinate_set.all())
|
||||
powerpaneltable.configure(request)
|
||||
powerfeedtable = PowerFeedCoordinateListTable(instance.powerfeedcoordinate_set.all())
|
||||
powerfeedtable.configure(request)
|
||||
table = CoordinateListTable(instance.coordinate_set.all())
|
||||
table.configure(request)
|
||||
|
||||
return {
|
||||
'circuitcoordinates_table': circuittable,
|
||||
'powerpanelcoordinates_table': powerpaneltable,
|
||||
'powerfeedcoordinates_table': powerfeedtable,
|
||||
'coordinates_table': table,
|
||||
}
|
||||
|
||||
@@ -925,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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user