Compare commits

..
9 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
16 changed files with 432 additions and 26 deletions
+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.9-beta1" 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")
+28 -1
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')
+39 -2
View File
@@ -35,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')),
@@ -258,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 = (
@@ -447,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',
), ),
), ),
@@ -548,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,
@@ -559,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),
),
]
+23
View File
@@ -41,6 +41,8 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, EventRulesMixin):
__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 @@
#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
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "3.9.0", "version": "3.9.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "3.9-beta1", "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",
@@ -79,7 +79,37 @@
<script type="text/javascript"> <script type="text/javascript">
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]
+55 -2
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,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:
@@ -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.9-beta1", 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",