Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8368ffa2fd | ||
|
|
23428c8ad2 | ||
|
|
c00b8c2cfe | ||
|
|
32003e59f9 | ||
|
|
bd32642b5c | ||
|
|
f463804087 | ||
|
|
b92b012309 | ||
|
|
74c295837c | ||
|
|
a543f5cb0c | ||
|
|
50c90a4328 | ||
|
|
374a88266a | ||
|
|
60cbc94a62 | ||
|
|
649305c83e | ||
|
|
b3397f4f4a | ||
|
|
a23604261a |
@@ -50,6 +50,8 @@ python3 manage.py collectstatic --no-input
|
|||||||
|
|
||||||
| netbox version | netbox-topology-views version |
|
| netbox version | netbox-topology-views version |
|
||||||
| -------------- | ----------------------------- |
|
| -------------- | ----------------------------- |
|
||||||
|
| >= 3.7.0 | >= v3.9.0 |
|
||||||
|
| >= 3.6.4 | >= v3.8.1 |
|
||||||
| >= 3.6.0 | >= v3.7.X |
|
| >= 3.6.0 | >= v3.7.X |
|
||||||
| >= 3.5.0 | >= v3.4.X |
|
| >= 3.5.0 | >= v3.4.X |
|
||||||
| >= 3.4.0 | >= v3.X.X |
|
| >= 3.4.0 | >= v3.X.X |
|
||||||
@@ -200,3 +202,12 @@ Set `Coordinate Groups` according to your needs:
|
|||||||
|
|
||||||
Set `Coordinates` according to your needs:
|
Set `Coordinates` according to your needs:
|
||||||
+ netbox_topology_views | coordinate | view/add/change/delete
|
+ 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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
|
|||||||
name = "netbox_topology_views"
|
name = "netbox_topology_views"
|
||||||
verbose_name = "Topology views"
|
verbose_name = "Topology views"
|
||||||
description = "An plugin to render topology maps"
|
description = "An plugin to render topology maps"
|
||||||
version = "3.8.1"
|
version = "3.9.1"
|
||||||
author = "Mattijs Vanhaverbeke"
|
author = "Mattijs Vanhaverbeke"
|
||||||
author_email = "author@example.com"
|
author_email = "author@example.com"
|
||||||
base_url = "netbox_topology_views"
|
base_url = "netbox_topology_views"
|
||||||
|
|||||||
@@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
|
|||||||
class IndividualOptionsSerializer(NetBoxModelSerializer):
|
class IndividualOptionsSerializer(NetBoxModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IndividualOptions
|
model = IndividualOptions
|
||||||
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "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", "draw_default_layout")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import sys
|
|||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
||||||
|
from extras.models import SavedFilter
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@@ -108,7 +109,30 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
|
|
||||||
if request.GET:
|
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, 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,show_neighbors = 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 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 show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
|
||||||
|
except SavedFilter.DoesNotExist: # filter_id not found
|
||||||
|
pass
|
||||||
|
except Exception as inst:
|
||||||
|
print(type(inst))
|
||||||
|
|
||||||
if 'group' not in request.query_params:
|
if 'group' not in request.query_params:
|
||||||
group_id = "default"
|
group_id = "default"
|
||||||
else:
|
else:
|
||||||
@@ -125,6 +149,9 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
show_circuit=show_circuit,
|
show_circuit=show_circuit,
|
||||||
show_power=show_power,
|
show_power=show_power,
|
||||||
show_wireless=show_wireless,
|
show_wireless=show_wireless,
|
||||||
|
group_sites=group_sites,
|
||||||
|
group_locations=group_locations,
|
||||||
|
group_racks=group_racks,
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
)
|
)
|
||||||
xml_data = export_data_to_xml(topo_data).decode('utf-8')
|
xml_data = export_data_to_xml(topo_data).decode('utf-8')
|
||||||
@@ -139,8 +166,8 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
|
|||||||
queryset = DeviceRole.objects.none()
|
queryset = DeviceRole.objects.none()
|
||||||
serializer_class = RoleImageSerializer
|
serializer_class = RoleImageSerializer
|
||||||
permission_required = (
|
permission_required = (
|
||||||
"dcim.add_device_role",
|
"dcim.add_devicerole",
|
||||||
"dcim.change_device_role",
|
"dcim.change_devicerole",
|
||||||
)
|
)
|
||||||
|
|
||||||
@action(detail=False, methods=["post"])
|
@action(detail=False, methods=["post"])
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from cProfile import label
|
from cProfile import label
|
||||||
import imp
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -36,7 +35,8 @@ class DeviceFilterForm(
|
|||||||
(None, ('q', 'filter_id', 'tag')),
|
(None, ('q', 'filter_id', 'tag')),
|
||||||
(_('Options'), (
|
(_('Options'), (
|
||||||
'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
|
||||||
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
|
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless',
|
||||||
|
'group_sites', 'group_locations', 'group_racks'
|
||||||
)),
|
)),
|
||||||
(_('Device'), ('id',)),
|
(_('Device'), ('id',)),
|
||||||
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
|
||||||
@@ -259,6 +259,15 @@ class DeviceFilterForm(
|
|||||||
show_wireless = forms.BooleanField(
|
show_wireless = forms.BooleanField(
|
||||||
label =_('Show Wireless Links'), required=False, initial=False
|
label =_('Show Wireless Links'), required=False, initial=False
|
||||||
)
|
)
|
||||||
|
group_sites = forms.BooleanField(
|
||||||
|
label =_('Group Sites'), required=False, initial=False
|
||||||
|
)
|
||||||
|
group_locations = forms.BooleanField(
|
||||||
|
label =_('Group Locations'), required=False, initial=False
|
||||||
|
)
|
||||||
|
group_racks = forms.BooleanField(
|
||||||
|
label =_('Group Racks'), required=False, initial=False
|
||||||
|
)
|
||||||
|
|
||||||
class CoordinateGroupsForm(NetBoxModelForm):
|
class CoordinateGroupsForm(NetBoxModelForm):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -448,6 +457,9 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
'show_circuit',
|
'show_circuit',
|
||||||
'show_power',
|
'show_power',
|
||||||
'show_wireless',
|
'show_wireless',
|
||||||
|
'group_sites',
|
||||||
|
'group_locations',
|
||||||
|
'group_racks',
|
||||||
'draw_default_layout',
|
'draw_default_layout',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -549,6 +561,27 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
help_text=_('Displays wireless connections. These connections are '
|
help_text=_('Displays wireless connections. These connections are '
|
||||||
'displayed as blue dotted lines.')
|
'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.')
|
||||||
|
)
|
||||||
draw_default_layout = forms.BooleanField(
|
draw_default_layout = forms.BooleanField(
|
||||||
label = ('Draw Default Layout'),
|
label = ('Draw Default Layout'),
|
||||||
required=False,
|
required=False,
|
||||||
@@ -560,5 +593,8 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = IndividualOptions
|
model = IndividualOptions
|
||||||
fields = [
|
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', 'draw_default_layout'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -13,7 +13,7 @@ from netbox.models import NetBoxModel
|
|||||||
from netbox.models.features import (
|
from netbox.models.features import (
|
||||||
ChangeLoggingMixin,
|
ChangeLoggingMixin,
|
||||||
ExportTemplatesMixin,
|
ExportTemplatesMixin,
|
||||||
WebhooksMixin,
|
EventRulesMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
from netbox_topology_views.utils import (
|
from netbox_topology_views.utils import (
|
||||||
@@ -26,7 +26,7 @@ from netbox_topology_views.utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["content_type", "object_id"]),
|
models.Index(fields=["content_type", "object_id"]),
|
||||||
@@ -41,6 +41,8 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
|||||||
|
|
||||||
__role: Optional[Role] = None
|
__role: Optional[Role] = None
|
||||||
|
|
||||||
|
_netbox_private = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def role(self) -> Role:
|
def role(self) -> Role:
|
||||||
if self.__role:
|
if self.__role:
|
||||||
@@ -111,6 +113,8 @@ class CoordinateGroup(NetBoxModel):
|
|||||||
blank = True,
|
blank = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_netbox_private = True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
|
||||||
@@ -138,6 +142,8 @@ class Coordinate(NetBoxModel):
|
|||||||
'Smaller values correspond to a position further up 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):
|
def get_or_create_default_group(group_id):
|
||||||
# Default group named "default" must always exist in order to make sure
|
# 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
|
# that coordinate values can be stored even if no coordinate group has been
|
||||||
@@ -187,6 +193,8 @@ class CircuitCoordinate(NetBoxModel):
|
|||||||
'Smaller values correspond to a position further up on the monitor.',
|
'Smaller values correspond to a position further up on the monitor.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_netbox_private = True
|
||||||
|
|
||||||
def get_or_create_default_group(group_id):
|
def get_or_create_default_group(group_id):
|
||||||
# Default group named "default" must always exist in order to make sure
|
# 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
|
# that coordinate values can be stored even if no coordinate group has been
|
||||||
@@ -236,6 +244,8 @@ class PowerPanelCoordinate(NetBoxModel):
|
|||||||
'Smaller values correspond to a position further up on the monitor.',
|
'Smaller values correspond to a position further up on the monitor.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_netbox_private = True
|
||||||
|
|
||||||
def get_or_create_default_group(group_id):
|
def get_or_create_default_group(group_id):
|
||||||
# Default group named "default" must always exist in order to make sure
|
# 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
|
# that coordinate values can be stored even if no coordinate group has been
|
||||||
@@ -285,6 +295,8 @@ class PowerFeedCoordinate(NetBoxModel):
|
|||||||
'Smaller values correspond to a position further up on the monitor.',
|
'Smaller values correspond to a position further up on the monitor.',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_netbox_private = True
|
||||||
|
|
||||||
def get_or_create_default_group(group_id):
|
def get_or_create_default_group(group_id):
|
||||||
# Default group named "default" must always exist in order to make sure
|
# 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
|
# that coordinate values can be stored even if no coordinate group has been
|
||||||
@@ -374,9 +386,20 @@ class IndividualOptions(NetBoxModel):
|
|||||||
show_wireless = models.BooleanField(
|
show_wireless = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
group_sites = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
group_locations = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
group_racks = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
draw_default_layout = models.BooleanField(
|
draw_default_layout = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_netbox_private = True
|
||||||
|
|
||||||
def __str___(self):
|
def __str___(self):
|
||||||
return f"{self.user_id}"
|
return f"{self.user_id}"
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ menu = PluginMenu(
|
|||||||
),
|
),
|
||||||
('PREFERENCES',
|
('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']),
|
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 {
|
#visgraph {
|
||||||
height: 64vh;
|
height: 64vh;
|
||||||
|
border: 1px solid #ced4da
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-netbox-color-mode=dark] #visgraph {
|
html[data-netbox-color-mode=dark] #visgraph {
|
||||||
background-color: #212529;
|
background-color: #212529;
|
||||||
|
border: 1px solid #495057
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.image-dropdown img {
|
.image-dropdown img {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
title: htmlTitle(node.title)
|
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
|
||||||
|
|
||||||
graph = new Network(container, { nodes, edges }, options)
|
graph = new Network(container, { nodes, edges }, options)
|
||||||
graph.fit()
|
graph.fit()
|
||||||
|
|
||||||
@@ -133,6 +138,189 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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); }
|
||||||
|
})
|
||||||
|
|
||||||
|
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 - 1) && canvascontext.pointer.canvas.x < (key.x2 + key.border / 2 + 1)
|
||||||
|
&& canvascontext.pointer.canvas.y > (key.y1 - key.border / 2 - 1) && canvascontext.pointer.canvas.y < (key.y2 + key.border / 2 + 1)) {
|
||||||
|
// 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 + 1) || canvascontext.pointer.canvas.x > (key.x2 - key.border / 2 - 1)
|
||||||
|
|| canvascontext.pointer.canvas.y < (key.y1 + key.border / 2 + 1) || canvascontext.pointer.canvas.y > (key.y2 - key.border / 2 - 1)) {
|
||||||
|
// 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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
graph.selectNodes(arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add information on which node belongs to which group (site/location/rack).
|
||||||
|
// 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/rack)
|
||||||
|
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: 12,
|
||||||
|
textPaddingY: 22,
|
||||||
|
font: "14px helvetica",
|
||||||
|
category: "Location"
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupedNodeRacks = combineNodeInfo('rack_id', 'rack');
|
||||||
|
let rackRectParams = {
|
||||||
|
lineWidth: "5",
|
||||||
|
color: "green",
|
||||||
|
paddingX: 70,
|
||||||
|
paddingY: 70,
|
||||||
|
textPaddingX: 8,
|
||||||
|
textPaddingY: 30,
|
||||||
|
font: "14px helvetica",
|
||||||
|
category: "Rack"
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
// Download Graph
|
// Download Graph
|
||||||
|
|||||||
+28
-53
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "3.7.0",
|
"version": "3.9.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "3.7.0",
|
"version": "3.9.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vis-data": "^7.1.6",
|
"vis-data": "^7.1.9",
|
||||||
"vis-network": "^9.1.6",
|
"vis-network": "^9.1.9",
|
||||||
"vis-util": "^5.0.3"
|
"vis-util": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.0",
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
@@ -102,7 +102,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anymatch": "~3.1.2",
|
"anymatch": "~3.1.2",
|
||||||
"braces": "~3.0.2",
|
"braces": "~3.0.2",
|
||||||
"fsevents": "~2.3.2",
|
|
||||||
"glob-parent": "~5.1.2",
|
"glob-parent": "~5.1.2",
|
||||||
"is-binary-path": "~2.1.0",
|
"is-binary-path": "~2.1.0",
|
||||||
"is-glob": "~4.0.1",
|
"is-glob": "~4.0.1",
|
||||||
@@ -405,29 +404,6 @@
|
|||||||
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
|
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
|
||||||
"@esbuild/linux-loong64": "0.14.54",
|
|
||||||
"esbuild-android-64": "0.14.54",
|
|
||||||
"esbuild-android-arm64": "0.14.54",
|
|
||||||
"esbuild-darwin-64": "0.14.54",
|
|
||||||
"esbuild-darwin-arm64": "0.14.54",
|
|
||||||
"esbuild-freebsd-64": "0.14.54",
|
|
||||||
"esbuild-freebsd-arm64": "0.14.54",
|
|
||||||
"esbuild-linux-32": "0.14.54",
|
|
||||||
"esbuild-linux-64": "0.14.54",
|
|
||||||
"esbuild-linux-arm": "0.14.54",
|
|
||||||
"esbuild-linux-arm64": "0.14.54",
|
|
||||||
"esbuild-linux-mips64le": "0.14.54",
|
|
||||||
"esbuild-linux-ppc64le": "0.14.54",
|
|
||||||
"esbuild-linux-riscv64": "0.14.54",
|
|
||||||
"esbuild-linux-s390x": "0.14.54",
|
|
||||||
"esbuild-netbsd-64": "0.14.54",
|
|
||||||
"esbuild-openbsd-64": "0.14.54",
|
|
||||||
"esbuild-sunos-64": "0.14.54",
|
|
||||||
"esbuild-windows-32": "0.14.54",
|
|
||||||
"esbuild-windows-64": "0.14.54",
|
|
||||||
"esbuild-windows-arm64": "0.14.54"
|
|
||||||
},
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -740,7 +716,8 @@
|
|||||||
"node_modules/timsort": {
|
"node_modules/timsort": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
"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": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
@@ -763,10 +740,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vis-data": {
|
"node_modules/vis-data": {
|
||||||
"version": "7.1.6",
|
"version": "7.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz",
|
||||||
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg==",
|
"integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==",
|
||||||
"hasInstallScript": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/visjs"
|
"url": "https://opencollective.com/visjs"
|
||||||
@@ -777,10 +753,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vis-network": {
|
"node_modules/vis-network": {
|
||||||
"version": "9.1.6",
|
"version": "9.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz",
|
||||||
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==",
|
"integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==",
|
||||||
"hasInstallScript": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/visjs"
|
"url": "https://opencollective.com/visjs"
|
||||||
@@ -789,16 +764,15 @@
|
|||||||
"@egjs/hammerjs": "^2.0.0",
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
"component-emitter": "^1.3.0",
|
"component-emitter": "^1.3.0",
|
||||||
"keycharm": "^0.2.0 || ^0.3.0 || ^0.4.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",
|
"uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
|
||||||
"vis-data": "^6.3.0 || ^7.0.0",
|
"vis-data": "^6.3.0 || ^7.0.0",
|
||||||
"vis-util": "^5.0.1"
|
"vis-util": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vis-util": {
|
"node_modules/vis-util": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz",
|
||||||
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
|
"integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
},
|
},
|
||||||
@@ -808,7 +782,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.0",
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
"component-emitter": "^1.3.0"
|
"component-emitter": "^1.3.0 || ^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1222,7 +1196,8 @@
|
|||||||
"timsort": {
|
"timsort": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||||
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
|
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"to-regex-range": {
|
"to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
@@ -1239,21 +1214,21 @@
|
|||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
},
|
},
|
||||||
"vis-data": {
|
"vis-data": {
|
||||||
"version": "7.1.6",
|
"version": "7.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz",
|
||||||
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg==",
|
"integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vis-network": {
|
"vis-network": {
|
||||||
"version": "9.1.6",
|
"version": "9.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz",
|
||||||
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==",
|
"integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"vis-util": {
|
"vis-util": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz",
|
||||||
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
|
"integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "3.8.1",
|
"version": "3.9.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bundle": "node bundle.js",
|
"bundle": "node bundle.js",
|
||||||
"bundle:styles": "node bundle.js --styles",
|
"bundle:styles": "node bundle.js --styles",
|
||||||
"bundle:scripts": "node bundle.js --scripts"
|
"bundle:scripts": "node bundle.js --scripts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vis-data": "^7.1.6",
|
"vis-data": "^7.1.9",
|
||||||
"vis-network": "^9.1.6",
|
"vis-network": "^9.1.9",
|
||||||
"vis-util": "^5.0.3"
|
"vis-util": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@egjs/hammerjs": "^2.0.0",
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
{% endblock controls %}
|
{% endblock controls %}
|
||||||
|
|
||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
<ul class="nav nav-tabs px-3">
|
<ul class="nav nav-tabs px-3" id="tabswrapper">
|
||||||
{% block tab_items %}
|
{% block tab_items %}
|
||||||
<li class="nav-item" role="presentation">
|
<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">
|
<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">
|
||||||
@@ -50,11 +50,13 @@
|
|||||||
|
|
||||||
{% block content-wrapper %}
|
{% block content-wrapper %}
|
||||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||||
<div class="tab-content">
|
<div class="tab-content" id="tabcontentwrapper">
|
||||||
|
|
||||||
{# Applied filters #}
|
{# Applied filters #}
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
{% applied_filters model filter_form request.GET %}
|
<div id="filterwrapper">
|
||||||
|
{% applied_filters model filter_form request.GET %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||||
@@ -78,6 +80,36 @@
|
|||||||
const brokenImage = '{{ broken_image }}';
|
const brokenImage = '{{ broken_image }}';
|
||||||
const topologyData = {{ topology_data | safe }};
|
const topologyData = {{ topology_data | safe }};
|
||||||
const basePath = '{{ basepath }}';
|
const basePath = '{{ basepath }}';
|
||||||
</script>
|
|
||||||
|
window.addEventListener("resize", resizeCanvas);
|
||||||
|
resizeCanvas();
|
||||||
|
|
||||||
|
function resizeCanvas() {
|
||||||
|
heightToSubtract = getElementHeight('.navbar') +
|
||||||
|
getElementHeight('.title-container') +
|
||||||
|
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>
|
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
|
||||||
{% endblock javascript %}
|
{% endblock javascript %}
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ def get_model_role(model: Type[Model]) -> Role:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_query_settings(request):
|
def get_query_settings(request):
|
||||||
|
filter_id = ''
|
||||||
|
if "filter_id" in request.GET:
|
||||||
|
filter_id = request.GET["filter_id"]
|
||||||
|
|
||||||
save_coords = False
|
save_coords = False
|
||||||
if "save_coords" in request.GET:
|
if "save_coords" in request.GET:
|
||||||
if request.GET["save_coords"] == "on":
|
if request.GET["save_coords"] == "on":
|
||||||
@@ -154,12 +158,27 @@ def get_query_settings(request):
|
|||||||
if request.GET["show_wireless"] == "on" :
|
if request.GET["show_wireless"] == "on" :
|
||||||
show_wireless = True
|
show_wireless = True
|
||||||
|
|
||||||
|
group_sites = False
|
||||||
|
if "group_sites" in request.GET:
|
||||||
|
if request.GET["group_sites"] == "on" :
|
||||||
|
group_sites = True
|
||||||
|
|
||||||
|
group_locations = False
|
||||||
|
if "group_locations" in request.GET:
|
||||||
|
if request.GET["group_locations"] == "on" :
|
||||||
|
group_locations = True
|
||||||
|
|
||||||
|
group_racks = False
|
||||||
|
if "group_racks" in request.GET:
|
||||||
|
if request.GET["group_racks"] == "on" :
|
||||||
|
group_racks = True
|
||||||
|
|
||||||
show_neighbors = False
|
show_neighbors = False
|
||||||
if "show_neighbors" in request.GET:
|
if "show_neighbors" in request.GET:
|
||||||
if request.GET["show_neighbors"] == "on" :
|
if request.GET["show_neighbors"] == "on" :
|
||||||
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
|
return filter_id, 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, show_neighbors
|
||||||
|
|
||||||
class LinePattern():
|
class LinePattern():
|
||||||
wireless = [2, 10, 2, 10]
|
wireless = [2, 10, 2, 10]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from django.db.models.functions import Lower
|
|||||||
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from extras.models import Tag
|
from extras.models import Tag, SavedFilter
|
||||||
from wireless.models import WirelessLink
|
from wireless.models import WirelessLink
|
||||||
from netbox.views.generic import (
|
from netbox.views.generic import (
|
||||||
ObjectView,
|
ObjectView,
|
||||||
@@ -180,6 +180,16 @@ def create_node(
|
|||||||
|
|
||||||
node["id"] = device.pk
|
node["id"] = device.pk
|
||||||
|
|
||||||
|
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.device_role.color != "":
|
if device.device_role.color != "":
|
||||||
node["color.border"] = "#" + device.device_role.color
|
node["color.border"] = "#" + device.device_role.color
|
||||||
|
|
||||||
@@ -325,6 +335,9 @@ def get_topology_data(
|
|||||||
show_neighbors: bool,
|
show_neighbors: bool,
|
||||||
show_power: bool,
|
show_power: bool,
|
||||||
show_wireless: bool,
|
show_wireless: bool,
|
||||||
|
group_sites: bool,
|
||||||
|
group_locations: bool,
|
||||||
|
group_racks: bool,
|
||||||
group_id,
|
group_id,
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -338,6 +351,7 @@ def get_topology_data(
|
|||||||
nodes_devices = {}
|
nodes_devices = {}
|
||||||
edges = []
|
edges = []
|
||||||
nodes = []
|
nodes = []
|
||||||
|
options = {}
|
||||||
edge_ids = 0
|
edge_ids = 0
|
||||||
nodes_circuits: Dict[int, Circuit] = {}
|
nodes_circuits: Dict[int, Circuit] = {}
|
||||||
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
||||||
@@ -651,6 +665,13 @@ def get_topology_data(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_locations:
|
||||||
|
options['group_locations'] = 'on'
|
||||||
|
if group_racks:
|
||||||
|
options['group_racks'] = 'on'
|
||||||
|
if group_sites:
|
||||||
|
options['group_sites'] = 'on'
|
||||||
|
|
||||||
for qs_device in queryset:
|
for qs_device in queryset:
|
||||||
if qs_device.pk not in nodes_devices and show_unconnected:
|
if qs_device.pk not in nodes_devices and show_unconnected:
|
||||||
nodes_devices[qs_device.pk] = qs_device
|
nodes_devices[qs_device.pk] = qs_device
|
||||||
@@ -663,6 +684,7 @@ def get_topology_data(
|
|||||||
results["nodes"] = nodes
|
results["nodes"] = nodes
|
||||||
results["edges"] = edges
|
results["edges"] = edges
|
||||||
results["group"] = group_id
|
results["group"] = group_id
|
||||||
|
results["options"] = options
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -688,8 +710,30 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
if request.GET:
|
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, 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, show_neighbors = 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 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 show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
|
||||||
|
except SavedFilter.DoesNotExist: # filter_id not found
|
||||||
|
pass
|
||||||
|
except Exception as inst:
|
||||||
|
print(type(inst))
|
||||||
|
|
||||||
if "group" not in request.GET:
|
if "group" not in request.GET:
|
||||||
group_id = "default"
|
group_id = "default"
|
||||||
else:
|
else:
|
||||||
@@ -708,6 +752,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
show_circuit=show_circuit,
|
show_circuit=show_circuit,
|
||||||
show_power=show_power,
|
show_power=show_power,
|
||||||
show_wireless=show_wireless,
|
show_wireless=show_wireless,
|
||||||
|
group_sites=group_sites,
|
||||||
|
group_locations=group_locations,
|
||||||
|
group_racks=group_racks,
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -729,6 +776,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
if individualOptions.show_circuit: q['show_circuit'] = "on"
|
if individualOptions.show_circuit: q['show_circuit'] = "on"
|
||||||
if individualOptions.show_power: q['show_power'] = "on"
|
if individualOptions.show_power: q['show_power'] = "on"
|
||||||
if individualOptions.show_wireless: q['show_wireless'] = "on"
|
if individualOptions.show_wireless: q['show_wireless'] = "on"
|
||||||
|
if individualOptions.group_sites: q['group_sites'] = "on"
|
||||||
|
if individualOptions.group_locations: q['group_locations'] = "on"
|
||||||
|
if individualOptions.group_racks: q['group_racks'] = "on"
|
||||||
if individualOptions.draw_default_layout:
|
if individualOptions.draw_default_layout:
|
||||||
q['draw_init'] = "true"
|
q['draw_init'] = "true"
|
||||||
else:
|
else:
|
||||||
@@ -771,9 +821,9 @@ ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
|
|||||||
class TopologyImagesView(PermissionRequiredMixin, View):
|
class TopologyImagesView(PermissionRequiredMixin, View):
|
||||||
permission_required = (
|
permission_required = (
|
||||||
"dcim.view_site",
|
"dcim.view_site",
|
||||||
"dcim.view_device_role",
|
"dcim.view_devicerole",
|
||||||
"dcim.add_device_role",
|
"dcim.add_devicerole",
|
||||||
"dcim.change_device_role",
|
"dcim.change_devicerole",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request: HttpRequest):
|
def get(self, request: HttpRequest):
|
||||||
@@ -1083,6 +1133,9 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
|
|||||||
'show_circuit': queryset.show_circuit,
|
'show_circuit': queryset.show_circuit,
|
||||||
'show_power': queryset.show_power,
|
'show_power': queryset.show_power,
|
||||||
'show_wireless': queryset.show_wireless,
|
'show_wireless': queryset.show_wireless,
|
||||||
|
'group_sites': queryset.group_sites,
|
||||||
|
'group_locations': queryset.group_locations,
|
||||||
|
'group_racks': queryset.group_racks,
|
||||||
'draw_default_layout': queryset.draw_default_layout,
|
'draw_default_layout': queryset.draw_default_layout,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ long_description = readme.read_text()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="netbox-topology-views",
|
name="netbox-topology-views",
|
||||||
version="3.8.1",
|
version="3.9.1",
|
||||||
description="An NetBox plugin to create Topology maps",
|
description="An NetBox plugin to create Topology maps",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
Reference in New Issue
Block a user