Skip to content

Commit f90bac8

Browse files
authored
Release 1.0.4 (#158)
* Code cleanup. * Minor bugfixes and visual enhancements in topoSphere.js. * Add plugin-specific filters support for Saved Filters.
1 parent 3cf3703 commit f90bac8

File tree

5 files changed

+72
-176
lines changed

5 files changed

+72
-176
lines changed

nextbox_ui_plugin/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class NextBoxUIConfig(PluginConfig):
1111
name = 'nextbox_ui_plugin'
1212
verbose_name = 'NextBox UI'
1313
description = 'A Next-Gen Topology Visualization Plugin for NetBox Powered by topoSphere SDK.'
14-
version = '1.0.3'
14+
version = '1.0.4'
1515
author = 'Igor Korotchenkov'
1616
author_email = '[email protected]'
1717
base_url = 'nextbox-ui'

nextbox_ui_plugin/static/nextbox_ui_plugin/modal.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11

22

3+
function decodeSanitizedString(sanitizedStr) {
4+
const parser = new DOMParser();
5+
const doc = parser.parseFromString(sanitizedStr, 'text/html');
6+
return doc.documentElement.textContent;
7+
}
8+
39
function showModal(titleConfig, tableData) {
410
const container = document.getElementById('topology-container');
511

@@ -100,13 +106,13 @@ function hideModal() {
100106
}
101107
}
102108

103-
window.addEventListener('topoSphere.nodeClicked', (event) => {
109+
function nodeClickHandler(event) {
104110
const { nodeId, nodeData, click } = event.detail;
105111
// Render Node modal window on right mouse button click only
106112
if (click.button !== 2) return;
107113
const titleConfig = {
108114
text: nodeData?.customAttributes?.name,
109-
href: nodeData?.customAttributes?.dcimDeviceLink,
115+
href: decodeSanitizedString(nodeData?.customAttributes?.dcimDeviceLink),
110116
}
111117
const tableContent = [
112118
['Model', nodeData?.customAttributes?.model || '–'],
@@ -115,14 +121,14 @@ window.addEventListener('topoSphere.nodeClicked', (event) => {
115121
['Primary IP', nodeData?.customAttributes?.primaryIP || '–'],
116122
]
117123
showModal(titleConfig, tableContent);
118-
});
124+
}
119125

120-
window.addEventListener('topoSphere.edgeClicked', (event) => {
126+
function edgeClickHandler(event) {
121127
const { edgeId, edgeData, click } = event.detail;
122128
// Render Edge modal window on right mouse button click only
123129
if (click.button !== 2) return;
124130
let linkName = edgeData?.customAttributes?.name;
125-
let linkHref = edgeData?.customAttributes?.dcimCableURL;
131+
let linkHref = decodeSanitizedString(edgeData?.customAttributes?.dcimCableURL);
126132
if (edgeData.isBundled) {
127133
linkName = 'LAG';
128134
linkHref = '';
@@ -136,4 +142,25 @@ window.addEventListener('topoSphere.edgeClicked', (event) => {
136142
['Target', edgeData?.customAttributes?.target || '–'],
137143
]
138144
showModal(titleConfig, tableContent);
145+
}
146+
147+
148+
window.addEventListener('topoSphere.nodeClicked', (event) => {
149+
event.preventDefault();
150+
nodeClickHandler(event);
151+
});
152+
153+
window.addEventListener('topoSphere.nodeDoubleTapped', (event) => {
154+
event.preventDefault();
155+
nodeClickHandler(event);
156+
});
157+
158+
window.addEventListener('topoSphere.edgeClicked', (event) => {
159+
event.preventDefault();
160+
edgeClickHandler(event);
161+
});
162+
163+
window.addEventListener('topoSphere.edgeDoubleTapped', (event) => {
164+
event.preventDefault();
165+
edgeClickHandler(event);
139166
});

nextbox_ui_plugin/static/nextbox_ui_plugin/topoSphere/topoSphere.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nextbox_ui_plugin/views.py

Lines changed: 36 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dcim.models import *
66
from ipam.models import *
77
from circuits.models import *
8-
from .models import SavedTopology
8+
from extras.models import SavedFilter
99
from . import forms, filters
1010
from django.contrib.auth.mixins import PermissionRequiredMixin
1111
from django.conf import settings
@@ -247,101 +247,6 @@ def filter_tags(tags):
247247
tags = filtered_tags
248248
return tags
249249

250-
def get_vlan_topology(nb_devices_qs, vlans):
251-
252-
topology_dict = {'nodes': [], 'edges': []}
253-
device_roles = set()
254-
all_device_tags = set()
255-
multi_cable_connections = []
256-
vlan = VLAN.objects.get(id=vlans)
257-
interfaces = vlan.get_interfaces()
258-
filtred_devices = [d.id for d in nb_devices_qs]
259-
filtred_interfaces = []
260-
for interface in interfaces:
261-
if interface.is_connectable:
262-
direct_device_id = interface.device.id
263-
interface_trace = interface.trace()
264-
if len(interface_trace) != 0:
265-
termination_b_iface = interface_trace[-1][-1]
266-
connected_device_id = termination_b_iface.device.id
267-
if (direct_device_id in filtred_devices) or (direct_device_id in filtred_devices):
268-
filtred_interfaces.append(interface)
269-
270-
271-
272-
devices = []
273-
for interface in filtred_interfaces:
274-
if interface.is_connectable:
275-
if interface.device not in devices:
276-
devices.append(interface.device)
277-
interface_trace = interface.trace()
278-
if len(interface_trace) != 0:
279-
termination_b_iface = interface_trace[-1][-1]
280-
if termination_b_iface.device not in devices:
281-
devices.append(termination_b_iface.device)
282-
283-
284-
device_ids = [d.id for d in devices]
285-
for device in devices:
286-
device_is_passive = False
287-
device_url = device.get_absolute_url()
288-
if NETBOX_CURRENT_VERSION >= version.parse("4.0.0"):
289-
device_role_obj = device.role
290-
else:
291-
device_role_obj = device.device_role
292-
primary_ip = ''
293-
if device.primary_ip:
294-
primary_ip = str(device.primary_ip.address)
295-
tags = [str(tag) for tag in device.tags.names()]
296-
for tag in tags:
297-
all_device_tags.add((tag, not tag_is_hidden(tag)))
298-
topology_dict['nodes'].append({
299-
'id': device.name,
300-
'name': device.name,
301-
'label': {'text': device.name},
302-
'dcimDeviceLink': device_url,
303-
'primaryIP': primary_ip,
304-
'serial_number': device.serial,
305-
'model': device.device_type.model,
306-
'deviceRole': device_role_obj.slug,
307-
'layer': get_node_layer_sort_preference(
308-
device_role_obj.slug
309-
),
310-
'iconName': get_icon_type(
311-
device.id
312-
),
313-
'isPassive': device_is_passive,
314-
'tags': tags,
315-
})
316-
is_visible = not (device_role_obj.slug in UNDISPLAYED_DEVICE_ROLE_SLUGS)
317-
device_roles.add((device_role_obj.slug, device_role_obj.name, is_visible))
318-
319-
mapped_links = []
320-
for interface in filtred_interfaces:
321-
if interface.is_connectable:
322-
interface_trace = interface.trace()
323-
if len(interface_trace) != 0:
324-
source_cable = interface_trace[0]
325-
dest_cable = interface_trace[-1]
326-
mapping_link = [source_cable[0].device.id,dest_cable[-1].device.id]
327-
if (mapping_link not in mapped_links) and (mapping_link.reverse() not in mapped_links):
328-
mapped_links.append(mapping_link)
329-
330-
topology_dict['edges'].append({
331-
'id': source_cable[1].id,
332-
'dcimCableURL': source_cable[1].get_absolute_url(),
333-
'label': f"Cable {source_cable[1].id}",
334-
'source': source_cable[0].device.name,
335-
'target': dest_cable[-1].device.name,
336-
'sourceDeviceName': source_cable[0].device.name,
337-
'targetDeviceName': dest_cable[-1].device.name,
338-
"srcIfName": if_shortname(source_cable[0].name),
339-
"tgtIfName": if_shortname(dest_cable[-1].name),
340-
})
341-
342-
return topology_dict, device_roles, multi_cable_connections, list(all_device_tags)
343-
344-
345250
def get_topology(nb_devices_qs, params):
346251
display_unconnected = params.get('display_unconnected')
347252
display_passive = params.get('display_passive')
@@ -403,7 +308,7 @@ def get_topology(nb_devices_qs, params):
403308
node_data = {
404309
'id': f'device-{nb_device.id}',
405310
'name': nb_device.name,
406-
'label': {'text': nb_device.name},
311+
'label': nb_device.name,
407312
'layer': get_node_layer_sort_preference(
408313
device_role_obj.slug
409314
),
@@ -460,8 +365,10 @@ def get_topology(nb_devices_qs, params):
460365
"label": f"Cable {link.id}",
461366
"source": f"device-{link.a_terminations[0].device.id}",
462367
"target": f"device-{link.b_terminations[0].device.id}",
463-
"sourceInterfaceLabel": if_shortname(link.a_terminations[0].name),
464-
"targetInterfaceLabel": if_shortname(link.b_terminations[0].name),
368+
"sourceInterface": link.a_terminations[0].name,
369+
"sourceInterfaceLabel": {'text': if_shortname(link.a_terminations[0].name)},
370+
"targetInterface": link.b_terminations[0].name,
371+
"targetInterfaceLabel": {'text': if_shortname(link.b_terminations[0].name)},
465372
"customAttributes": {
466373
"name": f"Cable {link.id}",
467374
"dcimCableURL": link_url,
@@ -512,10 +419,12 @@ def get_topology(nb_devices_qs, params):
512419
source_device_id = f"device-{side_a_interface.device.id}"
513420
target_device_id = f"device-{side_b_interface.device.id}"
514421
topology_dict['edges'].append({
515-
'source': source_device_id,
516-
'target': target_device_id,
517-
"sourceInterfaceLabel": if_shortname(side_a_interface.name),
518-
"targetInterfaceLabel": if_shortname(side_b_interface.name),
422+
"source": source_device_id,
423+
"target": target_device_id,
424+
"sourceInterface": link.a_terminations[0].name,
425+
"sourceInterfaceLabel": {'text': if_shortname(link.a_terminations[0].name)},
426+
"targetInterface": link.b_terminations[0].name,
427+
"targetInterfaceLabel": {'text': if_shortname(link.b_terminations[0].name)},
519428
"isLogicalMultiCable": True,
520429
"customAttributes": {
521430
"name": f"Multi-Cable Connection",
@@ -527,40 +436,6 @@ def get_topology(nb_devices_qs, params):
527436
return topology_dict, device_roles, multi_cable_connections, all_device_tags
528437

529438

530-
def get_saved_topology(id):
531-
topology_dict = {}
532-
device_roles = []
533-
device_tags = []
534-
device_roles_detailed = []
535-
device_tags_detailed = []
536-
layout_context = {}
537-
topology_data = SavedTopology.objects.get(id=id)
538-
if not topology_data:
539-
return topology_dict, device_roles, device_tags, layout_context
540-
topology_dict = dict(topology_data.topology)
541-
if 'nodes' not in topology_dict:
542-
return topology_dict, device_roles, device_tags, layout_context
543-
device_roles = list(set([str(d.get('deviceRole')) for d in topology_dict['nodes'] if d.get('deviceRole')]))
544-
for device_role in device_roles:
545-
is_visible = not (device_role in UNDISPLAYED_DEVICE_ROLE_SLUGS)
546-
device_role_obj = DeviceRole.objects.get(slug=device_role)
547-
if not device_role_obj:
548-
device_roles_detailed.append((device_role, device_role, is_visible))
549-
continue
550-
device_roles_detailed.append((device_role_obj.slug, device_role_obj.name, is_visible))
551-
device_roles_detailed.sort(key=lambda i: get_node_layer_sort_preference(i[0]))
552-
device_tags = set()
553-
for device in topology_dict['nodes']:
554-
if 'tags' not in device:
555-
continue
556-
for tag in device['tags']:
557-
device_tags.add(str(tag))
558-
device_tags = list(device_tags)
559-
device_tags_detailed = list([(tag, not tag_is_hidden(tag)) for tag in device_tags])
560-
layout_context = dict(topology_data.layout_context)
561-
return topology_dict, device_roles_detailed, device_tags_detailed, layout_context
562-
563-
564439
class TopologyView(PermissionRequiredMixin, View):
565440
"""Generic Topology View"""
566441
permission_required = ('dcim.view_site', 'dcim.view_device', 'dcim.view_cable')
@@ -570,45 +445,39 @@ class TopologyView(PermissionRequiredMixin, View):
570445

571446
def get(self, request):
572447

573-
if not request.GET:
574-
self.queryset = Device.objects.none()
575-
elif 'saved_topology_id' in request.GET:
448+
clean_request = request.GET.copy()
449+
450+
if not clean_request:
576451
self.queryset = Device.objects.none()
577452

578-
display_unconnected = request.GET.get('display_unconnected')
579-
if display_unconnected is not None:
580-
display_unconnected = display_unconnected.lower == 'true'
453+
self.queryset = self.filterset(clean_request, self.queryset).qs
581454

582-
display_passive = request.GET.get('display_passive')
583-
if display_passive is not None:
584-
display_passive = display_passive.lower() == 'true'
455+
saved_filter = None
456+
if 'filter_id' in clean_request and clean_request['filter_id']:
457+
filter_id = clean_request['filter_id']
458+
saved_filter = SavedFilter.objects.get(pk=filter_id)
459+
460+
if saved_filter:
461+
# Extract only plugin-specific filters from the SavedFilter.
462+
# All NetBox-native filters are handled by filtersets.
463+
display_unconnected = saved_filter.parameters.get('display_unconnected', [DISPLAY_UNCONNECTED])[0]
464+
display_passive = saved_filter.parameters.get('display_passive', [DISPLAY_PASSIVE_DEVICES])[0]
585465
else:
466+
display_unconnected = DISPLAY_UNCONNECTED
586467
display_passive = DISPLAY_PASSIVE_DEVICES
587468

588-
params = {
589-
'display_unconnected': display_unconnected,
590-
'display_passive': display_passive,
591-
}
469+
if clean_request.get('display_unconnected') is not None:
470+
display_unconnected = clean_request.get('display_unconnected')
592471

593-
saved_topology_id = request.GET.get('saved_topology_id')
594-
layout_context = {}
472+
if clean_request.get('display_passive') is not None:
473+
display_passive = clean_request.get('display_passive')
595474

596-
if saved_topology_id is not None:
597-
topology_dict, device_roles, device_tags, layout_context = get_saved_topology(saved_topology_id)
598-
else:
599-
vlans = []
600-
if 'vlan_id' in request.GET:
601-
clean_request = request.GET.copy()
602-
clean_request.pop('vlan_id')
603-
vlans = request.GET.get('vlan_id')
604-
else:
605-
clean_request = request.GET.copy()
475+
params = {
476+
'display_unconnected': str(display_unconnected).lower() == 'true',
477+
'display_passive': str(display_passive).lower() == 'true',
478+
}
606479

607-
self.queryset = self.filterset(clean_request, self.queryset).qs
608-
if len(vlans) == 0:
609-
topology_dict, device_roles, multi_cable_connections, device_tags = get_topology(self.queryset, params)
610-
else:
611-
topology_dict, device_roles, multi_cable_connections, device_tags = get_vlan_topology(self.queryset, vlans)
480+
topology_dict, device_roles, multi_cable_connections, device_tags = get_topology(self.queryset, params)
612481

613482
return render(request, self.template_name, {
614483
'source_data': json.dumps(topology_dict),

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
setup(
99
name='nextbox_ui_plugin',
10-
version='1.0.3',
10+
version='1.0.4',
1111
url='https://github.com/iDebugAll/nextbox-ui-plugin',
12-
download_url='https://github.com/iDebugAll/nextbox-ui-plugin/archive/v1.0.3.tar.gz',
12+
download_url='https://github.com/iDebugAll/nextbox-ui-plugin/archive/v1.0.4.tar.gz',
1313
description='A Next-Gen Topology Visualization Plugin for NetBox Powered by topoSphere SDK.',
1414
long_description=long_description,
1515
long_description_content_type='text/markdown',

0 commit comments

Comments
 (0)