Compare commits

...
17 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
mattieserver d9571b30b5 bump version (#410) 2023-10-23 19:17:22 +02:00
kkthxbye 76b43f5d1e Overwrite the form renderer of DeviceFilterForm to fix regression caused by netbox 3.6.4 (#408) 2023-10-20 06:38:33 +02:00
18 changed files with 492 additions and 98 deletions
+11
View File
@@ -50,6 +50,8 @@ python3 manage.py collectstatic --no-input
| 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.5.0 | >= v3.4.X |
| >= 3.4.0 | >= v3.X.X |
@@ -200,3 +202,12 @@ Set `Coordinate Groups` according to your needs:
Set `Coordinates` according to your needs:
+ netbox_topology_views | coordinate | view/add/change/delete
Set `Power Feed Coordinates` according to your needs:
+ netbox_topology_views | power feed coordinate | view/add/change/delete
Set `Power Panel Coordinates` according to your needs:
+ netbox_topology_views | power panel coordinate | view/add/change/delete
Set `Circuit Coordinates` according to your needs:
+ netbox_topology_views | circuit coordinate | view/add/change/delete
+1 -1
View File
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "3.8.0"
version = "3.9.1"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
+1 -1
View File
@@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "draw_default_layout")
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout")
+30 -3
View File
@@ -3,6 +3,7 @@ import sys
from circuits.models import Circuit
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
from extras.models import SavedFilter
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
@@ -108,7 +109,30 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
if request.GET:
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
filter_id, 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:
group_id = "default"
else:
@@ -125,6 +149,9 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
show_circuit=show_circuit,
show_power=show_power,
show_wireless=show_wireless,
group_sites=group_sites,
group_locations=group_locations,
group_racks=group_racks,
group_id=group_id,
)
xml_data = export_data_to_xml(topo_data).decode('utf-8')
@@ -139,8 +166,8 @@ class SaveRoleImageViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
queryset = DeviceRole.objects.none()
serializer_class = RoleImageSerializer
permission_required = (
"dcim.add_device_role",
"dcim.change_device_role",
"dcim.add_devicerole",
"dcim.change_devicerole",
)
@action(detail=False, methods=["post"])
+40 -3
View File
@@ -1,5 +1,4 @@
from cProfile import label
import imp
from django import forms
from django.conf import settings
@@ -30,12 +29,14 @@ class DeviceFilterForm(
ContactModelFilterForm,
NetBoxModelFilterSetForm
):
default_renderer = forms.renderers.DjangoTemplates()
model = Device
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
(_('Options'), (
'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
'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',)),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
@@ -258,6 +259,15 @@ class DeviceFilterForm(
show_wireless = forms.BooleanField(
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):
fieldsets = (
@@ -447,6 +457,9 @@ class IndividualOptionsForm(NetBoxModelForm):
'show_circuit',
'show_power',
'show_wireless',
'group_sites',
'group_locations',
'group_racks',
'draw_default_layout',
),
),
@@ -548,6 +561,27 @@ class IndividualOptionsForm(NetBoxModelForm):
help_text=_('Displays wireless connections. These connections are '
'displayed as blue dotted lines.')
)
group_sites = forms.BooleanField(
label =_('Group Sites'),
required=False,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same site.')
)
group_locations = forms.BooleanField(
label =_('Group Locations'),
required=False,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same location.')
)
group_racks = forms.BooleanField(
label =_('Group Racks'),
required=False,
initial=False,
help_text=_('Draws a rectangle around Devices that belong to the '
'same rack.')
)
draw_default_layout = forms.BooleanField(
label = ('Draw Default Layout'),
required=False,
@@ -559,5 +593,8 @@ class IndividualOptionsForm(NetBoxModelForm):
class Meta:
model = IndividualOptions
fields = [
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless', 'draw_default_layout'
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags',
'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections',
'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power',
'show_wireless', 'group_sites', 'group_locations', 'group_racks', '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 (
ChangeLoggingMixin,
ExportTemplatesMixin,
WebhooksMixin,
EventRulesMixin,
)
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:
indexes = [
models.Index(fields=["content_type", "object_id"]),
@@ -41,6 +41,8 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
__role: Optional[Role] = None
_netbox_private = True
@property
def role(self) -> Role:
if self.__role:
@@ -111,6 +113,8 @@ class CoordinateGroup(NetBoxModel):
blank = True,
)
_netbox_private = True
class Meta:
ordering = ['name']
@@ -138,6 +142,8 @@ class Coordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -187,6 +193,8 @@ class CircuitCoordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -236,6 +244,8 @@ class PowerPanelCoordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -285,6 +295,8 @@ class PowerFeedCoordinate(NetBoxModel):
'Smaller values correspond to a position further up on the monitor.',
)
_netbox_private = True
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
@@ -374,9 +386,20 @@ class IndividualOptions(NetBoxModel):
show_wireless = models.BooleanField(
default=False
)
group_sites = models.BooleanField(
default=False
)
group_locations = models.BooleanField(
default=False
)
group_racks = models.BooleanField(
default=False
)
draw_default_layout = models.BooleanField(
default=False
)
_netbox_private = True
def __str___(self):
return f"{self.user_id}"
+1 -1
View File
@@ -106,7 +106,7 @@ menu = PluginMenu(
),
('PREFERENCES',
(
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images", permissions=[ "dcim.view_site","dcim.view_device_role"]),
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images", permissions=[ "dcim.view_site","dcim.view_devicerole"]),
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options", permissions=['netbox_topology_views.change_individualoptions']),
),
),
@@ -1 +1 @@
#visgraph{height:64vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
#visgraph{height:64vh;border:1px solid #ced4da}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529;border:1px solid #495057}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
File diff suppressed because one or more lines are too long
@@ -1,12 +1,13 @@
#visgraph {
height: 64vh;
border: 1px solid #ced4da
}
html[data-netbox-color-mode=dark] #visgraph {
background-color: #212529;
border: 1px solid #495057
}
.image-dropdown img {
width: 64px;
height: 64px;
+188
View File
@@ -69,6 +69,11 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
title: htmlTitle(node.title)
}))
)
const group_sites = topologyData.options.group_sites
const group_locations = topologyData.options.group_locations
const group_racks = topologyData.options.group_racks
graph = new Network(container, { nodes, edges }, options)
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
+28 -53
View File
@@ -1,16 +1,16 @@
{
"name": "netbox_topology_views",
"version": "3.7.0",
"version": "3.9.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "netbox_topology_views",
"version": "3.7.0",
"version": "3.9.0",
"dependencies": {
"vis-data": "^7.1.6",
"vis-network": "^9.1.6",
"vis-util": "^5.0.3"
"vis-data": "^7.1.9",
"vis-network": "^9.1.9",
"vis-util": "^5.0.7"
},
"devDependencies": {
"@egjs/hammerjs": "^2.0.0",
@@ -102,7 +102,6 @@
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@@ -405,29 +404,6 @@
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
"dev": 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": {
"esbuild": "bin/esbuild"
},
@@ -740,7 +716,8 @@
"node_modules/timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
"dev": true
},
"node_modules/to-regex-range": {
"version": "5.0.1",
@@ -763,10 +740,9 @@
}
},
"node_modules/vis-data": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg==",
"hasInstallScript": true,
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz",
"integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/visjs"
@@ -777,10 +753,9 @@
}
},
"node_modules/vis-network": {
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==",
"hasInstallScript": true,
"version": "9.1.9",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz",
"integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/visjs"
@@ -789,16 +764,15 @@
"@egjs/hammerjs": "^2.0.0",
"component-emitter": "^1.3.0",
"keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0",
"timsort": "^0.3.0",
"uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"vis-data": "^6.3.0 || ^7.0.0",
"vis-util": "^5.0.1"
}
},
"node_modules/vis-util": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz",
"integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==",
"engines": {
"node": ">=8"
},
@@ -808,7 +782,7 @@
},
"peerDependencies": {
"@egjs/hammerjs": "^2.0.0",
"component-emitter": "^1.3.0"
"component-emitter": "^1.3.0 || ^2.0.0"
}
}
},
@@ -1222,7 +1196,8 @@
"timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
@@ -1239,21 +1214,21 @@
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"vis-data": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg==",
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.9.tgz",
"integrity": "sha512-COQsxlVrmcRIbZMMTYwD+C2bxYCFDNQ2EHESklPiInbD/Pk3JZ6qNL84Bp9wWjYjAzXfSlsNaFtRk+hO9yBPWA==",
"requires": {}
},
"vis-network": {
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g==",
"version": "9.1.9",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.9.tgz",
"integrity": "sha512-Ft+hLBVyiLstVYSb69Q1OIQeh3FeUxHJn0WdFcq+BFPqs+Vq1ibMi2sb//cxgq1CP7PH4yOXnHxEH/B2VzpZYA==",
"requires": {}
},
"vis-util": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.7.tgz",
"integrity": "sha512-E3L03G3+trvc/X4LXvBfih3YIHcKS2WrP0XTdZefr6W6Qi/2nNCqZfe4JFfJU6DcQLm6Gxqj2Pfl+02859oL5A==",
"requires": {}
}
}
@@ -1,16 +1,16 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "3.8.0",
"version": "3.9.1",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
"bundle:scripts": "node bundle.js --scripts"
},
"dependencies": {
"vis-data": "^7.1.6",
"vis-network": "^9.1.6",
"vis-util": "^5.0.3"
"vis-data": "^7.1.9",
"vis-network": "^9.1.9",
"vis-util": "^5.0.7"
},
"devDependencies": {
"@egjs/hammerjs": "^2.0.0",
@@ -29,7 +29,7 @@
{% endblock controls %}
{% block tabs %}
<ul class="nav nav-tabs px-3">
<ul class="nav nav-tabs px-3" id="tabswrapper">
{% block tab_items %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
@@ -50,11 +50,13 @@
{% block content-wrapper %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content">
<div class="tab-content" id="tabcontentwrapper">
{# Applied filters #}
{% if filter_form %}
{% applied_filters model filter_form request.GET %}
<div id="filterwrapper">
{% applied_filters model filter_form request.GET %}
</div>
{% endif %}
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
@@ -78,6 +80,36 @@
const brokenImage = '{{ broken_image }}';
const topologyData = {{ topology_data | safe }};
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>
{% endblock javascript %}
+20 -1
View File
@@ -107,6 +107,10 @@ def get_model_role(model: Type[Model]) -> Role:
)
def get_query_settings(request):
filter_id = ''
if "filter_id" in request.GET:
filter_id = request.GET["filter_id"]
save_coords = False
if "save_coords" in request.GET:
if request.GET["save_coords"] == "on":
@@ -154,12 +158,27 @@ def get_query_settings(request):
if request.GET["show_wireless"] == "on" :
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
if "show_neighbors" in request.GET:
if request.GET["show_neighbors"] == "on" :
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():
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.shortcuts import render, get_object_or_404
from django.views.generic import View
from extras.models import Tag
from extras.models import Tag, SavedFilter
from wireless.models import WirelessLink
from netbox.views.generic import (
ObjectView,
@@ -180,6 +180,16 @@ def create_node(
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 != "":
node["color.border"] = "#" + device.device_role.color
@@ -325,6 +335,9 @@ def get_topology_data(
show_neighbors: bool,
show_power: bool,
show_wireless: bool,
group_sites: bool,
group_locations: bool,
group_racks: bool,
group_id,
):
@@ -338,6 +351,7 @@ def get_topology_data(
nodes_devices = {}
edges = []
nodes = []
options = {}
edge_ids = 0
nodes_circuits: Dict[int, Circuit] = {}
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:
if qs_device.pk not in nodes_devices and show_unconnected:
nodes_devices[qs_device.pk] = qs_device
@@ -663,6 +684,7 @@ def get_topology_data(
results["nodes"] = nodes
results["edges"] = edges
results["group"] = group_id
results["options"] = options
return results
@@ -688,8 +710,30 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET:
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
filter_id, 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:
group_id = "default"
else:
@@ -708,6 +752,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
show_circuit=show_circuit,
show_power=show_power,
show_wireless=show_wireless,
group_sites=group_sites,
group_locations=group_locations,
group_racks=group_racks,
group_id=group_id,
)
@@ -729,6 +776,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if individualOptions.show_circuit: q['show_circuit'] = "on"
if individualOptions.show_power: q['show_power'] = "on"
if individualOptions.show_wireless: q['show_wireless'] = "on"
if individualOptions.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:
q['draw_init'] = "true"
else:
@@ -771,9 +821,9 @@ ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
class TopologyImagesView(PermissionRequiredMixin, View):
permission_required = (
"dcim.view_site",
"dcim.view_device_role",
"dcim.add_device_role",
"dcim.change_device_role",
"dcim.view_devicerole",
"dcim.add_devicerole",
"dcim.change_devicerole",
)
def get(self, request: HttpRequest):
@@ -1083,6 +1133,9 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
'show_circuit': queryset.show_circuit,
'show_power': queryset.show_power,
'show_wireless': queryset.show_wireless,
'group_sites': queryset.group_sites,
'group_locations': queryset.group_locations,
'group_racks': queryset.group_racks,
'draw_default_layout': queryset.draw_default_layout,
},
)
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup(
name="netbox-topology-views",
version="3.8.0",
version="3.9.1",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",