Compare commits

...
15 Commits
Author SHA1 Message Date
mattieserver 8368ffa2fd prepare 3.9.1 (#480) 2024-04-19 15:55:04 +02:00
Mario 23428c8ad2 set models to private (#476) 2024-04-01 23:26:46 +02:00
Mario c00b8c2cfe Fixes #236: Saved filter doesn't reload show cables (#475)
* fix restore saved filter options

* fix restore saved filter group options

* fix htmx export to xml
2024-03-30 17:29:26 +01:00
Mario 32003e59f9 fix power feeds attributeerror (#474) 2024-03-30 11:17:04 +01:00
Mario bd32642b5c refine group frame color (#472) 2024-03-30 10:13:29 +01:00
Mario f463804087 added group options to api (#466) 2024-03-08 16:19:55 +01:00
Mario b92b012309 Closes #329: Draw rectangles around Sites/Locations/Racks (#462) 2024-03-03 17:52:44 +01:00
mattieserver 74c295837c bump version (#449) 2024-01-13 21:59:37 +01:00
Mario a543f5cb0c fix pixel waste (#440) 2024-01-04 21:42:19 +01:00
mattieserver 50c90a4328 438 beta release for netbox v370 (#439)
* 3.9.0

* 3.9-beta1
2024-01-02 12:22:46 +01:00
mattieserver 374a88266a used EventRulesMixin (#437) 2024-01-02 11:42:47 +01:00
mattieserver 60cbc94a62 remove imp (#426) 2024-01-02 11:27:46 +01:00
Mario 649305c83e Fixes #416: Images View permissions incorrect (#418)
* Add resize listener and height calculation

* Fixed typos
2023-10-29 14:58:17 +01:00
Mario b3397f4f4a Fixes #417: README is missing some permissions (#419)
* Add resize listener and height calculation

* Fixed typos

* add permissions for new views
2023-10-29 14:56:42 +01:00
Mario a23604261a Update README.md (#415)
Version matrix wasn't in sync with the latest release.
2023-10-29 14:56:19 +01:00
18 changed files with 491 additions and 98 deletions
+11
View File
@@ -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
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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")
+30 -3
View File
@@ -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"])
+38 -2
View File
@@ -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
@@ -37,6 +36,7 @@ class DeviceFilterForm(
(_('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),
),
]
+25 -2
View File
@@ -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}"
+1 -1
View File
@@ -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;
+188
View File
@@ -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
View File
@@ -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 %}
+20 -1
View File
@@ -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]
+58 -5
View File
@@ -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,7 +710,29 @@ 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"
@@ -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,
}, },
) )
+1 -1
View File
@@ -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",