Compare commits
9
Commits
v3.9-beta1
..
v3.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8368ffa2fd | ||
|
|
23428c8ad2 | ||
|
|
c00b8c2cfe | ||
|
|
32003e59f9 | ||
|
|
bd32642b5c | ||
|
|
f463804087 | ||
|
|
b92b012309 | ||
|
|
74c295837c | ||
|
|
a543f5cb0c |
@@ -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"
|
||||||
|
|||||||
@@ -50,4 +50,4 @@ class PowerFeedCoordinateSerializer(NetBoxModelSerializer):
|
|||||||
class IndividualOptionsSerializer(NetBoxModelSerializer):
|
class IndividualOptionsSerializer(NetBoxModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = IndividualOptions
|
model = IndividualOptions
|
||||||
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "draw_default_layout")
|
fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import sys
|
|||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
||||||
|
from extras.models import SavedFilter
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
@@ -108,7 +109,30 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
|
|
||||||
if request.GET:
|
if request.GET:
|
||||||
|
|
||||||
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
|
filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks,show_neighbors = get_query_settings(request)
|
||||||
|
|
||||||
|
# Read options from saved filters as NetBox does not handle custom plugin filters
|
||||||
|
if "filter_id" in request.GET and request.GET["filter_id"] != '':
|
||||||
|
try:
|
||||||
|
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
||||||
|
saved_filter_params = getattr(saved_filter, 'parameters')
|
||||||
|
|
||||||
|
if save_coords == False and 'save_coords' in saved_filter_params: save_coords = saved_filter_params['save_coords']
|
||||||
|
if show_power == False and 'show_power' in saved_filter_params: show_power = saved_filter_params['show_power']
|
||||||
|
if show_circuit == False and 'show_circuit' in saved_filter_params: show_circuit = saved_filter_params['show_circuit']
|
||||||
|
if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
|
||||||
|
if show_single_cable_logical_conns == False and 'show_single_cable_logical_conns' in saved_filter_params: show_single_cable_logical_conns = saved_filter_params['show_single_cable_logical_conns']
|
||||||
|
if show_cables == False and 'show_cables' in saved_filter_params: show_cables = saved_filter_params['show_cables']
|
||||||
|
if show_wireless == False and 'show_wireless' in saved_filter_params: show_wireless = saved_filter_params['show_wireless']
|
||||||
|
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
|
||||||
|
if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations']
|
||||||
|
if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks']
|
||||||
|
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
|
||||||
|
except SavedFilter.DoesNotExist: # filter_id not found
|
||||||
|
pass
|
||||||
|
except Exception as inst:
|
||||||
|
print(type(inst))
|
||||||
|
|
||||||
if 'group' not in request.query_params:
|
if 'group' not in request.query_params:
|
||||||
group_id = "default"
|
group_id = "default"
|
||||||
else:
|
else:
|
||||||
@@ -125,6 +149,9 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
|
|||||||
show_circuit=show_circuit,
|
show_circuit=show_circuit,
|
||||||
show_power=show_power,
|
show_power=show_power,
|
||||||
show_wireless=show_wireless,
|
show_wireless=show_wireless,
|
||||||
|
group_sites=group_sites,
|
||||||
|
group_locations=group_locations,
|
||||||
|
group_racks=group_racks,
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
)
|
)
|
||||||
xml_data = export_data_to_xml(topo_data).decode('utf-8')
|
xml_data = export_data_to_xml(topo_data).decode('utf-8')
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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 %}
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ def get_model_role(model: Type[Model]) -> Role:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_query_settings(request):
|
def get_query_settings(request):
|
||||||
|
filter_id = ''
|
||||||
|
if "filter_id" in request.GET:
|
||||||
|
filter_id = request.GET["filter_id"]
|
||||||
|
|
||||||
save_coords = False
|
save_coords = False
|
||||||
if "save_coords" in request.GET:
|
if "save_coords" in request.GET:
|
||||||
if request.GET["save_coords"] == "on":
|
if request.GET["save_coords"] == "on":
|
||||||
@@ -154,12 +158,27 @@ def get_query_settings(request):
|
|||||||
if request.GET["show_wireless"] == "on" :
|
if request.GET["show_wireless"] == "on" :
|
||||||
show_wireless = True
|
show_wireless = True
|
||||||
|
|
||||||
|
group_sites = False
|
||||||
|
if "group_sites" in request.GET:
|
||||||
|
if request.GET["group_sites"] == "on" :
|
||||||
|
group_sites = True
|
||||||
|
|
||||||
|
group_locations = False
|
||||||
|
if "group_locations" in request.GET:
|
||||||
|
if request.GET["group_locations"] == "on" :
|
||||||
|
group_locations = True
|
||||||
|
|
||||||
|
group_racks = False
|
||||||
|
if "group_racks" in request.GET:
|
||||||
|
if request.GET["group_racks"] == "on" :
|
||||||
|
group_racks = True
|
||||||
|
|
||||||
show_neighbors = False
|
show_neighbors = False
|
||||||
if "show_neighbors" in request.GET:
|
if "show_neighbors" in request.GET:
|
||||||
if request.GET["show_neighbors"] == "on" :
|
if request.GET["show_neighbors"] == "on" :
|
||||||
show_neighbors = True
|
show_neighbors = True
|
||||||
|
|
||||||
return save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors
|
return filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors
|
||||||
|
|
||||||
class LinePattern():
|
class LinePattern():
|
||||||
wireless = [2, 10, 2, 10]
|
wireless = [2, 10, 2, 10]
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from django.db.models.functions import Lower
|
|||||||
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from extras.models import Tag
|
from extras.models import Tag, SavedFilter
|
||||||
from wireless.models import WirelessLink
|
from wireless.models import WirelessLink
|
||||||
from netbox.views.generic import (
|
from netbox.views.generic import (
|
||||||
ObjectView,
|
ObjectView,
|
||||||
@@ -180,6 +180,16 @@ def create_node(
|
|||||||
|
|
||||||
node["id"] = device.pk
|
node["id"] = device.pk
|
||||||
|
|
||||||
|
if device.site is not None:
|
||||||
|
node["site"] = device.site.name
|
||||||
|
node["site_id"] = device.site_id
|
||||||
|
if device.location is not None:
|
||||||
|
node["location"] = device.location.name
|
||||||
|
node["location_id"] = device.location_id
|
||||||
|
if device.rack is not None:
|
||||||
|
node["rack"] = device.rack.name
|
||||||
|
node["rack_id"] = device.rack_id
|
||||||
|
|
||||||
if device.device_role.color != "":
|
if device.device_role.color != "":
|
||||||
node["color.border"] = "#" + device.device_role.color
|
node["color.border"] = "#" + device.device_role.color
|
||||||
|
|
||||||
@@ -325,6 +335,9 @@ def get_topology_data(
|
|||||||
show_neighbors: bool,
|
show_neighbors: bool,
|
||||||
show_power: bool,
|
show_power: bool,
|
||||||
show_wireless: bool,
|
show_wireless: bool,
|
||||||
|
group_sites: bool,
|
||||||
|
group_locations: bool,
|
||||||
|
group_racks: bool,
|
||||||
group_id,
|
group_id,
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -338,6 +351,7 @@ def get_topology_data(
|
|||||||
nodes_devices = {}
|
nodes_devices = {}
|
||||||
edges = []
|
edges = []
|
||||||
nodes = []
|
nodes = []
|
||||||
|
options = {}
|
||||||
edge_ids = 0
|
edge_ids = 0
|
||||||
nodes_circuits: Dict[int, Circuit] = {}
|
nodes_circuits: Dict[int, Circuit] = {}
|
||||||
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
||||||
@@ -651,6 +665,13 @@ def get_topology_data(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if group_locations:
|
||||||
|
options['group_locations'] = 'on'
|
||||||
|
if group_racks:
|
||||||
|
options['group_racks'] = 'on'
|
||||||
|
if group_sites:
|
||||||
|
options['group_sites'] = 'on'
|
||||||
|
|
||||||
for qs_device in queryset:
|
for qs_device in queryset:
|
||||||
if qs_device.pk not in nodes_devices and show_unconnected:
|
if qs_device.pk not in nodes_devices and show_unconnected:
|
||||||
nodes_devices[qs_device.pk] = qs_device
|
nodes_devices[qs_device.pk] = qs_device
|
||||||
@@ -663,6 +684,7 @@ def get_topology_data(
|
|||||||
results["nodes"] = nodes
|
results["nodes"] = nodes
|
||||||
results["edges"] = edges
|
results["edges"] = edges
|
||||||
results["group"] = group_id
|
results["group"] = group_id
|
||||||
|
results["options"] = options
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -688,8 +710,30 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
|
|
||||||
if request.GET:
|
if request.GET:
|
||||||
|
|
||||||
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
|
filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors = get_query_settings(request)
|
||||||
|
|
||||||
|
# Read options from saved filters as NetBox does not handle custom plugin filters
|
||||||
|
if "filter_id" in request.GET and request.GET["filter_id"] != '':
|
||||||
|
try:
|
||||||
|
saved_filter = SavedFilter.objects.get(pk=filter_id)
|
||||||
|
saved_filter_params = getattr(saved_filter, 'parameters')
|
||||||
|
|
||||||
|
if save_coords == False and 'save_coords' in saved_filter_params: save_coords = saved_filter_params['save_coords']
|
||||||
|
if show_power == False and 'show_power' in saved_filter_params: show_power = saved_filter_params['show_power']
|
||||||
|
if show_circuit == False and 'show_circuit' in saved_filter_params: show_circuit = saved_filter_params['show_circuit']
|
||||||
|
if show_logical_connections == False and 'show_logical_connections' in saved_filter_params: show_logical_connections = saved_filter_params['show_logical_connections']
|
||||||
|
if show_single_cable_logical_conns == False and 'show_single_cable_logical_conns' in saved_filter_params: show_single_cable_logical_conns = saved_filter_params['show_single_cable_logical_conns']
|
||||||
|
if show_cables == False and 'show_cables' in saved_filter_params: show_cables = saved_filter_params['show_cables']
|
||||||
|
if show_wireless == False and 'show_wireless' in saved_filter_params: show_wireless = saved_filter_params['show_wireless']
|
||||||
|
if group_sites == False and 'group_sites' in saved_filter_params: group_sites = saved_filter_params['group_sites']
|
||||||
|
if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations']
|
||||||
|
if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks']
|
||||||
|
if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors']
|
||||||
|
except SavedFilter.DoesNotExist: # filter_id not found
|
||||||
|
pass
|
||||||
|
except Exception as inst:
|
||||||
|
print(type(inst))
|
||||||
|
|
||||||
if "group" not in request.GET:
|
if "group" not in request.GET:
|
||||||
group_id = "default"
|
group_id = "default"
|
||||||
else:
|
else:
|
||||||
@@ -708,6 +752,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
show_circuit=show_circuit,
|
show_circuit=show_circuit,
|
||||||
show_power=show_power,
|
show_power=show_power,
|
||||||
show_wireless=show_wireless,
|
show_wireless=show_wireless,
|
||||||
|
group_sites=group_sites,
|
||||||
|
group_locations=group_locations,
|
||||||
|
group_racks=group_racks,
|
||||||
group_id=group_id,
|
group_id=group_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -729,6 +776,9 @@ class TopologyHomeView(PermissionRequiredMixin, View):
|
|||||||
if individualOptions.show_circuit: q['show_circuit'] = "on"
|
if individualOptions.show_circuit: q['show_circuit'] = "on"
|
||||||
if individualOptions.show_power: q['show_power'] = "on"
|
if individualOptions.show_power: q['show_power'] = "on"
|
||||||
if individualOptions.show_wireless: q['show_wireless'] = "on"
|
if individualOptions.show_wireless: q['show_wireless'] = "on"
|
||||||
|
if individualOptions.group_sites: q['group_sites'] = "on"
|
||||||
|
if individualOptions.group_locations: q['group_locations'] = "on"
|
||||||
|
if individualOptions.group_racks: q['group_racks'] = "on"
|
||||||
if individualOptions.draw_default_layout:
|
if individualOptions.draw_default_layout:
|
||||||
q['draw_init'] = "true"
|
q['draw_init'] = "true"
|
||||||
else:
|
else:
|
||||||
@@ -1083,6 +1133,9 @@ class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
|
|||||||
'show_circuit': queryset.show_circuit,
|
'show_circuit': queryset.show_circuit,
|
||||||
'show_power': queryset.show_power,
|
'show_power': queryset.show_power,
|
||||||
'show_wireless': queryset.show_wireless,
|
'show_wireless': queryset.show_wireless,
|
||||||
|
'group_sites': queryset.group_sites,
|
||||||
|
'group_locations': queryset.group_locations,
|
||||||
|
'group_racks': queryset.group_racks,
|
||||||
'draw_default_layout': queryset.draw_default_layout,
|
'draw_default_layout': queryset.draw_default_layout,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ long_description = readme.read_text()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="netbox-topology-views",
|
name="netbox-topology-views",
|
||||||
version="3.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",
|
||||||
|
|||||||
Reference in New Issue
Block a user