Skip to content

Commit 57ea538

Browse files
authored
Topology Viewer Page (#41)
* Introduce a dedicated Topology Viewer page - A plugin code has been reworked to support generic topology page generation. - A new dedicated page has been added to display an arbitrary topology based on Search Form input (#30). The page is located under . - The Topology Viewer page is now being listed under the Plugins menu. - A Site Topology modal window header now points to to be able to jump to a larger window quickly. - A canvas size on a Topology Viewer page is now being calculated based on the window size (#33). * Update README.md Typo fixes. * Improve Topology Viewer page layout * Add new Topology Viewer page screenshot * Update README.md Add comments on new Topology Viewer page.
1 parent ca6d1c9 commit 57ea538

File tree

11 files changed

+231
-39
lines changed

11 files changed

+231
-39
lines changed

README.md

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -186,18 +186,18 @@ Default mapping already contains some general categories:
186186
The Plugin can control the visibility of the layers and/or specific nodes on the topology view.<br/>
187187
The visibility control is currently implemented for specific device roles, device tags, unconnected devices, and passive devices:<br/>
188188

189-
- Inifial visibility behavior for specific device roles is controlled by 'undisplayed_device_role_slugs' plugin parameter. Listed device role slugs are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.<br/>
189+
- Initial visibility behavior for specific device roles is controlled by 'undisplayed_device_role_slugs' plugin parameter. Listed device role slugs are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.<br/>
190190

191-
- Inifial visibility behavior for specific device tags is controlled by 'undisplayed_device_tags' plugin parameter. Devices with tags matching listed tag resular expressions are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.<br/>
191+
- Initial visibility behavior for specific device tags is controlled by 'undisplayed_device_tags' plugin parameter. Devices with tags matching listed tag regular expressions are hidden on initial view load, you may then hide/display any layer with a control button on the topology view page.<br/>
192192
By default, the plugin lists all discovered device tags in Select Layers menu. You can use 'select_layers_list_include_device_tags' and 'select_layers_list_exclude_device_tags' plugin parameters to filter the included tags.<br/>
193-
All three tag visibility control parameters are optional lists of regular expressions. Tags matching 'undisplayed_device_tags' are always listed in Select Layers menu. Empty or unset 'select_layers_list_include_device_tags' allows all discovered tags to be listed in Select layers menu. If set, 'select_layers_list_include_device_tags' works as an allow list for matched tags. 'select_layers_list_exclude_device_tags' filters out matched tags from the list, expept for ones matching 'undisplayed_device_tags'.
193+
All three tag visibility control parameters are optional lists of regular expressions. Tags matching 'undisplayed_device_tags' are always listed in Select Layers menu. Empty or unset 'select_layers_list_include_device_tags' allows all discovered tags to be listed in Select layers menu. If set, 'select_layers_list_include_device_tags' works as an allow list for matched tags. 'select_layers_list_exclude_device_tags' filters out matched tags from the list, excpept for ones matching 'undisplayed_device_tags'.
194194

195195
- Initial visibility behavior for unconnected nodes is controlled by DISPLAY_UNCONNECTED boolean plugin parameter.<br/>
196-
By default unconnected nodes are being displayed. Set DISPLAY_UNCONNECTED to False to hide them on initial topology view load.<br/>
196+
By default, unconnected nodes are being displayed. Set DISPLAY_UNCONNECTED to False to hide them on initial topology view load.<br/>
197197
A separate 'Hide/Display Unconnected' button may then be used to hide or display those nodes.
198198

199-
- Initical visibility for passive devices (patch pannels, PDUs) is controlled by DISPLAY_PASSIVE_DEVICES boolean plugin parameter. A device is considered passive if it has cables connected to Front and Rear Ports only and not to Interfaces.<br/>Passive devices are hidden by default. You can display them with 'Display Passive Devices' button on the topology view page. <br/>
200-
Actual multi-cable connections between the end-devices a replaced by the direct logical connection once the passive devices are hidden. This logical direct link may be displayed regardless of the passive devices visibility in addition to the cabling across patch pannels if you set DISPLAY_LOGICAL_MULTICABLE_LINKS plugin paramenter to True. DISPLAY_LOGICAL_MULTICABLE_LINKS is set to False by default. This parameter only affects the initical logical link visibility. With hidden passive devices, it is always being displayed.<br/>
199+
- Initial visibility for passive devices (patch panels, PDUs) is controlled by DISPLAY_PASSIVE_DEVICES boolean plugin parameter. A device is considered passive if it has cables connected to Front and Rear Ports only and not to Interfaces.<br/>Passive devices are hidden by default. You can display them with 'Display Passive Devices' button on the topology view page. <br/>
200+
Actual multi-cable connections between the end-devices a replaced by the direct logical connection once the passive devices are hidden. This logical direct link may be displayed regardless of the passive device visibility in addition to the cabling across patch panels if you set DISPLAY_LOGICAL_MULTICABLE_LINKS plugin parameter to True. DISPLAY_LOGICAL_MULTICABLE_LINKS is set to False by default. This parameter only affects the initial logical link visibility. With hidden passive devices, it is always being displayed.<br/>
201201
<br/>
202202

203203
Device layers are ordered automatically by default. You can control this behavior with INITIAL_LAYOUT plugin parameter. Valid options are 'vertical', 'horizontal', and 'auto'.<br/>
@@ -221,7 +221,7 @@ sudo systemctl restart netbox
221221
# Installation with Docker
222222
The Plugin may be installed in a Netbox Docker deployment.
223223
The package contains a Dockerfile for [Netbox-Community Docker](https://github.com/netbox-community/netbox-docker) extension. Latest-LDAP version is used by default as a source.<br/>
224-
Download the Plugin and build from source:
224+
Download the Plugin and build from the source:
225225
```
226226
$ git clone https://github.com/iDebugAll/nextbox-ui-plugin
227227
$ cd nextbox-ui-plugin
@@ -241,21 +241,25 @@ $ cd netbox-docker
241241
$ docker-compose down
242242
$ docker-compose up -d
243243
```
244-
Netbox Community Docker setup performs static files collection on every startup. No manual actions required.
244+
Netbox Community Docker setup performs static file collection on every startup. No manual actions are required.
245245

246246
# Usage
247247

248-
Once installed and initialized, the Plugin runs on a backend. It currently supports topology visualization for Netbox Sites.<br/>
248+
Once installed and initialized, the Plugin runs on a backend.<br/>
249+
The Plugin supports a topology visualization of arbitrary sets of Sites and Regions.<br/>
249250
<br/>
250-
Site topology visualization may be accessed in two different ways:
251+
You can access Topology visualizations in different ways:
251252
1. By clicking a custom plugin Topology button on a Site page.
252253
![](samples/sample_topology_button.png)
253-
The topology visualization will open in a pop-up window:
254+
The Site topology visualization will open in a pop-up window:
254255
![](samples/sample_topology_view.png)<br/>
255256
Nodes are draggable and clickable:
256257
![](samples/sample_node_tooltip_content.png)<br/>
257258
You can switch between vertical and horizontal layers sort order back and forth. Default is vertical.<br/>
258-
2. Directly via /plugins/nextbox-ui/site_topology/{site_id}. This is helpful in case if you need an embedded topology frame on some of your side resources.
259+
260+
2. Using Plugins dropdown menu item: *Plugins -> NextBox UI -> Topology Viewer*.<br/>
261+
Use Search form controls to pick desired Sites, Regions, or Devices.<br/>
262+
![](samples/sample_topology_viewer_page01.png)
259263
<br/>
260264

261265
### Visibility control
@@ -276,7 +280,7 @@ If DISPLAY_LOGICAL_MULTICABLE_LINKS is set to True (default is False) this logic
276280
![](samples/sample_display_logical_link.png)
277281

278282
### Required Netbox User Permissions
279-
The Plugin requires the following user permissions in order to access topology view:
283+
The Plugin requires the following user permissions to access the topology view:
280284

281285
- dcim | site | Can read site
282286
- dcim | device | Can view device

nextbox_ui_plugin/filters.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import django_filters
2+
from dcim.models import Device, Site, Region
3+
4+
5+
class TopologyFilterSet(django_filters.FilterSet):
6+
7+
device_id = django_filters.ModelMultipleChoiceFilter(
8+
queryset=Device.objects.all(),
9+
to_field_name='id',
10+
field_name='id',
11+
label='Device (ID)',
12+
)
13+
site_id = django_filters.ModelMultipleChoiceFilter(
14+
queryset=Site.objects.all(),
15+
label='Site (ID)',
16+
)
17+
region_id = django_filters.ModelMultipleChoiceFilter(
18+
queryset=Region.objects.all(),
19+
field_name='site__region',
20+
label='Region (ID)',
21+
)
22+
23+
class Meta:
24+
model = Device
25+
fields = ['id', 'name', ]

nextbox_ui_plugin/forms.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django import forms
2+
from utilities.forms import (
3+
BootstrapMixin, DynamicModelMultipleChoiceField,
4+
)
5+
from dcim.models import Device, Site, Region
6+
7+
8+
class TopologyFilterForm(BootstrapMixin, forms.Form):
9+
10+
model = Device
11+
12+
device_id = DynamicModelMultipleChoiceField(
13+
queryset=Device.objects.all(),
14+
to_field_name='id',
15+
required=False,
16+
null_option='None',
17+
)
18+
site_id = DynamicModelMultipleChoiceField(
19+
queryset=Site.objects.all(),
20+
required=False,
21+
to_field_name='id',
22+
null_option='None',
23+
)
24+
region_id = DynamicModelMultipleChoiceField(
25+
queryset=Region.objects.all(),
26+
required=False,
27+
to_field_name='id',
28+
null_option='None',
29+
)

nextbox_ui_plugin/navigation.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from extras.plugins import PluginMenuItem
2+
3+
4+
menu_items = (
5+
PluginMenuItem(
6+
link='plugins:nextbox_ui_plugin:topology',
7+
link_text='Topology Viewer',
8+
buttons=()
9+
),
10+
)

nextbox_ui_plugin/static/nextbox_ui_plugin/next_app.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@
22
/**
33
* NeXt UI base application
44
*/
5+
6+
// Calculate topology canvas size
7+
// based on window size during page loading.
8+
// Canvas size can only be set once during page init.
9+
// It does not autoscale. Page reload is required to update topology dimensions.
10+
if (document.body.clientWidth && document.body.clientHeight) {
11+
var canvasWidth = document.body.clientWidth*0.75;
12+
var canvasHeight = document.body.clientHeight*0.75;
13+
} else {
14+
var canvasWidth = 850;
15+
var canvasHeight = 750;
16+
};
17+
518
// Initialize topology
619
var topo = new nx.graphic.Topology({
720
// View dimensions
8-
width: 850,
9-
height: 700,
21+
width: canvasWidth,
22+
height: canvasHeight,
1023
// Dataprocessor is responsible for spreading
1124
// the Nodes across the view.
1225
// 'force' dataprocessor spreads the Nodes so
@@ -67,6 +80,9 @@
6780

6881
var Shell = nx.define(nx.ui.Application, {
6982
methods: {
83+
getContainer: function() {
84+
return new nx.dom.Element(document.getElementById('nx-ui-topology'));
85+
},
7086
start: function () {
7187
// Read topology data from variable
7288
topo.data(topologyData);
@@ -80,7 +96,6 @@
8096
});
8197
topo.activateLayout('hierarchicalLayout');
8298
};
83-
8499
// Attach it to the document
85100
topo.attach(this);
86101
}
@@ -349,6 +364,9 @@
349364

350365
// Create an application instance
351366
var shell = new Shell();
367+
shell.on('resize', function() {
368+
topo && topo.adaptToContainer();
369+
});
352370
// Run the application
353371
shell.start();
354372
})(nx);

nextbox_ui_plugin/templates/nextbox_ui_plugin/site_topo_button.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
<div class="modal-content">
99
<div class="modal-header">
1010
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
11-
<h2 class="modal-title" style="text-align: center;">{{ object.name }} Topology</h2>
11+
<h2 class="modal-title" style="text-align: center;"><a href={% url 'plugins:nextbox_ui_plugin:topology' %}?site_id={{ object.id }}>{{ object.name }} Topology</a></h2>
1212
</div>
13-
<iframe src="{% url 'plugins:nextbox_ui_plugin:site_topology' site_id=object.id %}" style="height: 75vh; width: 100%;"></iframe>
13+
<iframe src="{% url 'plugins:nextbox_ui_plugin:site_topology' %}?site_id={{ object.id }}" style="height: 75vh; width: 100%;"></iframe>
1414
</div>
1515
</div>
1616
</div>

nextbox_ui_plugin/templates/nextbox_ui_plugin/site_topology.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<script src="{% static 'nextbox_ui_plugin/button_utils.js' %}"></script>
1313
</head>
1414
<body>
15-
<div class="container-sm ml-4 my-sm-3">
15+
<div class="container-sm ml-4 my-sm-3" id="nx-ui-topology">
1616
<div class="row">
1717
<div class="col-xs-4">
1818
<div class="btn-group">
@@ -58,6 +58,7 @@ <h6 class="dropdown-header">Device Tags</h6>
5858
</div>
5959
</div>
6060
</div>
61+
6162
<script type="text/javascript">
6263
var displayUnconnected = {{ display_unconnected|yesno:"true,false" }};
6364
var displayPassiveDevices = {{ display_passive_devices|yesno:"true,false" }};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{% extends 'base.html' %}
2+
{% load buttons %}
3+
{% load static %}
4+
5+
{% block header %}
6+
<script src="{% static 'nextbox_ui_plugin/next_sources/js/next.js' %}"></script>
7+
<link rel="stylesheet"href="{% static 'nextbox_ui_plugin/next_sources/css/next.css' %}">
8+
<script src="{% static 'nextbox_ui_plugin/button_utils.js' %}"></script>
9+
{% endblock %}
10+
11+
{% block content %}
12+
13+
<h2>Topology Viewer</h2>
14+
15+
<div class="col-md-3 pull-right right-side-panel noprint" style="z-index: 2;">
16+
{% include 'inc/search_panel.html' %}
17+
{% block sidebar %}{% endblock %}
18+
</div>
19+
<div>
20+
<div class="btn-group">
21+
<button class="btn btn-primary " onclick='horizontal()'>Horizontal Layout</button>
22+
<button class="btn btn-primary" onclick="vertical()">Vertical Layout</button>
23+
</div>
24+
<input class="btn btn-primary" type="button" id="showHideUndonnectedButton" value="" onclick="return showHideUndonnectedButtonOnClick(this);" />
25+
<input class="btn btn-primary" type="button" id="showHidePassiveDevicesButton" value="" onclick="return showHidePassiveDevicesButtonOnClick(this);" />
26+
<div class="btn-group">
27+
<div class="dropdown">
28+
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
29+
Select Layers
30+
</button>
31+
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
32+
<h6 class="dropdown-header">Device Roles</h6>
33+
<div class="dropdown-divider"></div>
34+
{% for device_role_slug, device_role_name, is_visible in device_roles %}
35+
<div class="checkbox ml-1 my-1">
36+
<label>
37+
<input onchange="layerSelectorOnChange(this)" type="checkbox" value="{{ device_role_slug }}" {{ is_visible|yesno:"checked," }}>{{ device_role_name }}
38+
</label>
39+
</div>
40+
{% endfor %}
41+
<div class="dropdown-divider"></div>
42+
{% if device_tags %}
43+
<h6 class="dropdown-header">Device Tags</h6>
44+
{% for tag, is_visible in device_tags %}
45+
<div class="checkbox ml-1 my-1">
46+
<label>
47+
<input onchange="layerSelectorByTagOnChange(this)" type="checkbox" value="{{ tag }}" {{ is_visible|yesno:"checked," }}>{{ tag }}
48+
</label>
49+
</div>
50+
{% endfor %}
51+
{% endif %}
52+
<div class="dropdown-divider"></div>
53+
</div>
54+
</div>
55+
</div>
56+
</div>
57+
<div class="row noprint" style="z-index: 1;">
58+
<div class="col-lg-3" style="z-index: 1;">
59+
<div class="pull-left noprint">
60+
<div id="nx-ui-topology" class="container">
61+
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
67+
68+
{% endblock %}
69+
70+
{% block javascript %}
71+
72+
73+
<script type="text/javascript">
74+
var displayUnconnected = {{ display_unconnected|yesno:"true,false" }};
75+
var displayPassiveDevices = {{ display_passive_devices|yesno:"true,false" }};
76+
var displayLogicalMultiCableLinks = {{ display_logical_multicable_links|yesno:"true,false" }};
77+
var undisplayedRoles = {{ undisplayed_roles|safe }};
78+
var undisplayedDeviceTags = {{ undisplayed_device_tags|safe }};
79+
var topologyData = {{ source_data|safe }};
80+
var initialLayout = '{{ initial_layout|safe }}';
81+
console.log(topologyData);
82+
</script>
83+
<script src="{% static 'nextbox_ui_plugin/next_app.js' %}"></script>
84+
85+
<script type="text/javascript">
86+
showHideUndonnectedButtonInitial();
87+
showHidePassiveDevicesButtonInitial();
88+
</script>
89+
90+
{% endblock %}
91+
92+
93+

nextbox_ui_plugin/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
from . import views
33

44
urlpatterns = [
5-
path('site_topology/<int:site_id>/', views.TopologyView.as_view(), name='site_topology'),
5+
path('site_topology/', views.SiteTopologyView.as_view(), name='site_topology'),
6+
path('topology/', views.TopologyView.as_view(), name='topology'),
67
]

0 commit comments

Comments
 (0)