Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #329: Draw rectangles around Sites/Locations/Racks #462

Merged
merged 8 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion netbox_topology_views/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ class Meta:
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")
41 changes: 39 additions & 2 deletions netbox_topology_views/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class DeviceFilterForm(
(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')),
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -447,6 +457,9 @@ class IndividualOptionsForm(NetBoxModelForm):
'show_circuit',
'show_power',
'show_wireless',
'group_sites',
'group_locations',
'group_racks',
'draw_default_layout',
),
),
Expand Down Expand Up @@ -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,
Expand All @@ -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'
]
Original file line number Diff line number Diff line change
@@ -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),
),
]
9 changes: 9 additions & 0 deletions netbox_topology_views/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,15 @@ 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
)
Expand Down
22 changes: 11 additions & 11 deletions netbox_topology_views/static/netbox_topology_views/js/app.js

Large diffs are not rendered by default.

188 changes: 188 additions & 0 deletions netbox_topology_views/static_dev/js/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const csrftoken = getCookie('csrftoken')
// Render vis graph
let graph = null // vis graph instance

let searchParams = new URLSearchParams(window.location.search)
let group_sites = searchParams.get('group_sites')
let group_locations = searchParams.get('group_locations')
let group_racks = searchParams.get('group_racks')

const container = document.querySelector('#visgraph')
const coordSaveCheckbox = document.querySelector('#id_save_coords')
;(function handleLoadData() {
Expand Down Expand Up @@ -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: "yellow",
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
Expand Down
17 changes: 16 additions & 1 deletion netbox_topology_views/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,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 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]
Expand Down
Loading