Compare commits

...
15 Commits
Author SHA1 Message Date
Mattijs Vanhaverbeke 2edd597871 bump version 2020-07-20 15:49:06 +02:00
mattieserver 5f4f97c360 Merge pull request #41 from mattieserver/32
added Filter topology via URL Parameter
2020-07-20 15:00:10 +02:00
Mattijs Vanhaverbeke 17ae8b4a1d #32 2020-07-20 14:57:09 +02:00
mattieserver bb9765e011 Merge pull request #40 from mattieserver/37
add filter by region
2020-07-20 12:38:15 +02:00
Mattijs Vanhaverbeke 5bfc47775b #37 2020-07-20 12:36:38 +02:00
mattieserver 216508170f Merge pull request #39 from mattieserver/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-20 11:07:20 +02:00
dependabot[bot] 0faa135978 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 07:57:54 +00:00
mattieserver 9ba61cce28 Merge pull request #28 from mattieserver/22052020_dev
22052020 dev
2020-05-24 18:31:07 +02:00
mattieserver 9824a3f659 Merge branch 'master' into 22052020_dev 2020-05-24 18:30:59 +02:00
mattieserver 4115584306 Update README.md 2020-05-23 18:20:04 +02:00
mattieserver cd239ac6ae Update README.md 2020-05-23 18:19:13 +02:00
mattieserver 030844f2fb Merge pull request #27 from ryanmerolle/rm-issue-26
Update README to include local_requirements setup
2020-05-23 18:15:56 +02:00
ryanmerolle 2e6dd7de23 Update README to include local_requirements setup 2020-05-23 12:04:19 -04:00
Mattijs Vanhaverbeke de5707ff06 PATCH instead of POST 2020-05-22 12:57:58 +02:00
Mattijs Vanhaverbeke 371c5c3502 added download button, fixed perms 2020-05-22 12:07:46 +02:00
14 changed files with 933 additions and 254 deletions
+25 -2
View File
@@ -10,9 +10,22 @@ Support to filter on name, site, tag and device role.
## Install ## Install
The plugin is available as a Python package and can be installed with pip.
Run `pip install netbox-topology-views` in your virtual env. Run `pip install netbox-topology-views` in your virtual env.
Add 'netbox_topology_views' to the PLUGINS array in your django configuration file. To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
```no-highlight
# echo netbox-topology-views >> local_requirements.txt
```
Once installed, the plugin needs to be enabled in your `configuration.py`
```python
# In your configuration.py
PLUGINS = ["netbox_topology_views"]
```
Then run `python3 manage.py collectstatic --no-input` Then run `python3 manage.py collectstatic --no-input`
@@ -63,4 +76,14 @@ Run `pip install netbox-topology-views --upgrade` in your venv.
Run `python3 manage.py collectstatic --no-input` Run `python3 manage.py collectstatic --no-input`
Clear your browser cache.
Clear you browser cache.
### Permissions
To view `/plugins/topology-views/` you need the following permissions:
+ dcim | device | can view device
+ dcim | site | can view site
+ extras | tag | can view tag
+ dcim | device role | can view device role
+1 -1
View File
@@ -4,7 +4,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 toplogoy maps' description = 'An plugin to render toplogoy maps'
version = '0.4.8' version = '0.4.9'
author = 'Mattijs Vanhaverbeke' author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com' author_email = 'author@example.com'
base_url = 'topology-views' base_url = 'topology-views'
+27 -22
View File
@@ -30,11 +30,11 @@ class PreSelectTagsViewSet(ReadOnlyModelViewSet):
queryset = Tag.objects.filter(name__in=preselected_tags) queryset = Tag.objects.filter(name__in=preselected_tags)
serializer_class = PreTagSerializer serializer_class = PreTagSerializer
class SaveCoordsViewSet(GenericViewSet): class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.all() queryset = Device.objects.all()
serializer_class = TopologyDummySerializer serializer_class = TopologyDummySerializer
@action(detail=False, methods=['post']) @action(detail=False, methods=['patch'])
def save_coords(self, request): def save_coords(self, request):
results = {} results = {}
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]: if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
@@ -76,14 +76,14 @@ class SaveCoordsViewSet(GenericViewSet):
results["status"] = "not allowed to save coords" results["status"] = "not allowed to save coords"
return Response(results, status=500) return Response(results, status=500)
class SearchViewSet(GenericViewSet): class SearchViewSet(ReadOnlyModelViewSet):
#_ignore_model_permissions = True #_ignore_model_permissions = True
#permission_classes = [IsAuthenticatedOrLoginNotRequired] #permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = Device.objects.all() queryset = Device.objects.all()
serializer_class = TopologyDummySerializer serializer_class = TopologyDummySerializer
def _filter(self, site, role, name, tags): def _filter(self, site, role, name, tags, region):
filter_devices = Device.objects.all() filter_devices = Device.objects.all()
if name is not None: if name is not None:
filter_devices = filter_devices.filter(name__contains=name) filter_devices = filter_devices.filter(name__contains=name)
@@ -93,29 +93,34 @@ class SearchViewSet(GenericViewSet):
filter_devices = filter_devices.filter(device_role__id__in=role) filter_devices = filter_devices.filter(device_role__id__in=role)
if tags is not None: if tags is not None:
filter_devices = filter_devices.filter(tags__id__in=tags) filter_devices = filter_devices.filter(tags__id__in=tags)
if region is not None:
filter_devices = filter_devices.filter(site__region__id__in=region)
return filter_devices return filter_devices
@action(detail=False, methods=['post']) @action(detail=False, methods=['get'])
def search(self, request): def search(self, request):
name = None name = request.query_params.get('name', None)
sites = None if name == "":
devicerole = None name = None
tags = None
if "devicerole" in request.data:
if request.data["devicerole"]:
devicerole = request.data["devicerole"]
if "sites" in request.data:
if request.data["sites"]:
sites = request.data["sites"]
if "name" in request.data:
if request.data["name"]:
name = request.data["name"]
if "tags" in request.data:
if request.data["tags"]:
tags = request.data["tags"]
devices = self._filter(sites, devicerole, name, tags) sites = request.query_params.getlist('sites[]', None)
if sites == []:
sites = None
devicerole = request.query_params.getlist('devicerole[]', None)
if devicerole == []:
devicerole = None
tags = request.query_params.getlist('tags[]', None)
if tags == []:
tags = None
regions = request.query_params.getlist('regions[]', None)
if regions == []:
regions = None
devices = self._filter(sites, devicerole, name, tags, regions)
nodes = [] nodes = []
edges = [] edges = []
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,33 @@
var graph = null;
var container = null;
function iniPlotboxFull() {
document.addEventListener('DOMContentLoaded', function () {
container = document.getElementById('fullvisgraph');
startRender();
}, false);
}
function startRender() {
var url = location.search;
$.ajax({
type: "GET",
url: "../../api/plugins/topology-views/search/search/" + url,
contentType: "application/json; charset=utf-8",
success: function (data_result, status, xhr) {
graph = null;
nodes = new vis.DataSet();
edges = new vis.DataSet();
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
$.each(data_result["nodes"], function (index, device) {
nodes.add(device);
});
$.each(data_result["edges"], function (index, edge) {
edges.add(edge);
});
graph.fit();
canvas = document.getElementById('fullvisgraph').getElementsByTagName('canvas')[0];
}
});
}
+91 -10
View File
@@ -1,5 +1,8 @@
var graph = null; var graph = null;
var container = null; var container = null;
var downloadButton = null;
var MIME_TYPE = "image/png";
var canvas = null;
var csrftoken = null; var csrftoken = null;
var nodes = new vis.DataSet(); var nodes = new vis.DataSet();
var edges = new vis.DataSet(); var edges = new vis.DataSet();
@@ -29,6 +32,7 @@ var options = {
solver: 'forceAtlas2Based' solver: 'forceAtlas2Based'
} }
}; };
var selected_regions = [];
function iniPlotboxIndex() { function iniPlotboxIndex() {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
@@ -36,6 +40,8 @@ function iniPlotboxIndex() {
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
startLoadSearchBar(); startLoadSearchBar();
handleButtonPress(); handleButtonPress();
downloadButton = document.getElementById('btnDownloadImage');
btnFullView = document.getElementById('btnFullView');
}, false); }, false);
} }
@@ -73,11 +79,23 @@ function startLoadSearchBar() {
theme: "bootstrap", theme: "bootstrap",
multiple: true, multiple: true,
ajax: { ajax: {
url: "../../api/dcim/sites/?brief=true", url: function (params) {
var base_url = "../../api/dcim/sites/?brief=true";
if (selected_regions.length == 0) {
return base_url;
}
else {
for (var i = 0; i < selected_regions.length; i++) {
var tmp = "&region_id=" + selected_regions[i];
base_url = base_url + tmp;
}
return base_url;
}
},
dataType: "json", dataType: "json",
type: "GET", type: "GET",
data: function (params) { data: function (params) {
var queryParameters = { var queryParameters = {
q: params.term q: params.term
} }
@@ -122,6 +140,53 @@ function startLoadSearchBar() {
} }
} }
}); });
$('#regions').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
multiple: true,
ajax: {
url: "../../api/dcim/regions/?brief=true",
dataType: "json",
type: "GET",
data: function (params) {
var queryParameters = {
q: params.term
}
return queryParameters;
},
processResults: function (data) {
return {
results: $.map(data.results, function (item) {
return {
text: item.name,
id: item.id
}
})
};
}
}
});
$('#regions').on('select2:select', function (e) {
var data = e.params.data;
var index = selected_regions.indexOf(data.id.toString());
if (index == -1) {
selected_regions.push(data.id.toString());
}
});
$('#regions').on('select2:unselect', function (e) {
var data = e.params.data;
var index = selected_regions.indexOf(data.id.toString());
console.log(index);
if (index > -1) {
selected_regions.splice(index, 1);
}
});
$('#regions').on('select2:clear', function (e) {
selected_regions = [];
});
var deviceRolesSelect = $('#device-roles'); var deviceRolesSelect = $('#device-roles');
$.ajax({ $.ajax({
@@ -166,19 +231,29 @@ function handleButtonPress() {
var value2 = $("#device-roles").val(); var value2 = $("#device-roles").val();
var value3 = $("#sites").val(); var value3 = $("#sites").val();
var value4 = $("#tags").val(); var value4 = $("#tags").val();
var value5 = $("#regions").val();
$.ajax({ $.ajax({
type: "POST", type: "GET",
url: "../../api/plugins/topology-views/search/search/", url: "../../api/plugins/topology-views/search/search/",
data: JSON.stringify({ data: {
'name': value, 'name': value,
'devicerole': value2, 'devicerole[]': value2,
'sites': value3, 'sites[]': value3,
'tags': value4 'tags[]': value4,
}), 'regions[]': value5
},
headers: { "X-CSRFToken": csrftoken }, headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
success: function (data_result) { beforeSend: function(){
new_url = this.url.split("../../api/plugins/topology-views/search/search/?");
new_url = new_url[1];
new_url = "../../plugins/topology-views/full?" + new_url
btnFullView.classList.remove("disabled");
btnFullView.href = new_url;
},
success: function (data_result, status, xhr) {
$("#status").html('<span class="label label-info">Drawing network</span>'); $("#status").html('<span class="label label-info">Drawing network</span>');
graph = null; graph = null;
nodes = new vis.DataSet(); nodes = new vis.DataSet();
@@ -191,9 +266,13 @@ function handleButtonPress() {
edges.add(edge); edges.add(edge);
}); });
graph.fit(); graph.fit();
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
graph.on('afterDrawing', function () { graph.on('afterDrawing', function () {
$("#status").html('<span class="label label-success">Ready</span>'); $("#status").html('<span class="label label-success">Ready</span>');
var image = canvas.toDataURL(MIME_TYPE);
downloadButton.href = image;
downloadButton.download = "topology";
}); });
graph.on("dragEnd", function (params) { graph.on("dragEnd", function (params) {
@@ -204,10 +283,11 @@ function handleButtonPress() {
//nodes.update({ id: node_id, physics: false }); //nodes.update({ id: node_id, physics: false });
$.ajax({ $.ajax({
url: "../../api/plugins/topology-views/save-coords/save_coords/", url: "../../api/plugins/topology-views/save-coords/save_coords/",
type: 'POST', type: 'PATCH',
dataType: 'json', dataType: 'json',
headers: { "X-CSRFToken": csrftoken }, headers: { "X-CSRFToken": csrftoken },
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
processData: false,
data: JSON.stringify({ data: JSON.stringify({
'node_id': node_id, 'node_id': node_id,
'x': coordinates.x, 'x': coordinates.x,
@@ -223,6 +303,7 @@ function handleButtonPress() {
} }
}); });
}); });
}, },
error: function (error_result) { error: function (error_result) {
$("#status").html('<span class="label label-warning">Something went wrong</span>'); $("#status").html('<span class="label label-warning">Something went wrong</span>');
@@ -0,0 +1,24 @@
{% load static %}
{% load helpers %}
{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
<div class="panel-body">
<div id="fullvisgraph" class=""></div>
</div>
{% endwith %}
{% endblock %}
{% block javascript %}
<script src="{% static 'jquery/jquery-3.4.1.min.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
<script type="application/javascript"> iniPlotboxFull()</script>
{% endblock %}
@@ -12,6 +12,14 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Network</strong> <span id="status"></span> <strong>Network</strong> <span id="status"></span>
<div class="buttons pull-right">
<a id="btnDownloadImage" class="btn btn-xs btn-info">
<i class="fa fa-download"></i>
</a>
<a id="btnFullView" class="btn btn-xs btn-info disabled" target="_blank">
<i class="fa fa-share"></i>
</a>
</div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div id="visgraph" class=""></div> <div id="visgraph" class=""></div>
@@ -41,6 +49,10 @@
<div class="form-group"> <div class="form-group">
<label for="tags">Tags</label> <label for="tags">Tags</label>
<select class="form-control" multiple="multiple" id="tags"></select> <select class="form-control" multiple="multiple" id="tags"></select>
</div>
<div class="form-group">
<label for="regions">Regions</label>
<select class="form-control" multiple="multiple" id="regions"></select>
</div> </div>
<hr class="mb-4"> <hr class="mb-4">
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button> <button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
+1
View File
@@ -7,4 +7,5 @@ from . import views
# a specific view so that it can be accessed by users. # a specific view so that it can be accessed by users.
urlpatterns = ( urlpatterns = (
path('', views.TopologyHomeView.as_view(), name='home'), path('', views.TopologyHomeView.as_view(), name='home'),
path('full', views.TopologyFullView.as_view(), name='full'),
) )
+10 -1
View File
@@ -9,4 +9,13 @@ class TopologyHomeView(PermissionRequiredMixin, View):
Show the home page Show the home page
""" """
def get(self, request): def get(self, request):
return render(request, 'netbox_topology_views/index.html') return render(request, 'netbox_topology_views/index.html')
class TopologyFullView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_site', 'dcim.view_device')
"""
Show the full view page
"""
def get(self, request):
return render(request, 'netbox_topology_views/full.html')
+692 -211
View File
File diff suppressed because it is too large Load Diff
+14 -4
View File
@@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "0.4.8", "version": "0.4.9",
"scripts": { "scripts": {
"resources": "gulp build", "resources": "gulp build",
"resources_dev": "gulp build_dev" "resources_dev": "gulp build_dev"
@@ -9,10 +9,20 @@
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0", "gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-sass": "^4.0.2", "gulp-sass": "^4.1.0",
"gulp-uglify": "^3.0.2", "gulp-uglify": "^3.0.2",
"vis-network": "^7.6.4" "vis-network": "^7.10.0",
"keycharm": "^0.3.0",
"moment": "^2.24.0",
"timsort": "^0.3.0",
"vis-data": "^6.2.1",
"vis-util": "^4.0.0",
"uuid": "7.0.0",
"@egjs/hammerjs" : "^2.0.0"
},
"peerDependencies": {
} }
} }
+1 -1
View File
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup( setup(
name='netbox-topology-views', name='netbox-topology-views',
version='0.4.8', version='0.4.9',
description='An NetBox plugin to create Topology maps', description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views', url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke', author='Mattijs Vanhaverbeke',