Compare commits
4
Commits
v4.3.0
...
v1.0.0-alpha.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b53ceed2ae | ||
|
|
064751f727 | ||
|
|
deedee78bd | ||
|
|
683e125cd5 |
@@ -35,6 +35,7 @@ Then run `python3 manage.py collectstatic --no-input`
|
|||||||
|
|
||||||
| netbox version | netbox-topology-views version |
|
| netbox version | netbox-topology-views version |
|
||||||
| ------------- |-------------|
|
| ------------- |-------------|
|
||||||
|
| >= 3.1.8 | >= v1.0.0a1 |
|
||||||
| >= 2.11.1 | >= v0.5.3 |
|
| >= 2.11.1 | >= v0.5.3 |
|
||||||
| >= 2.10.0 | >= v0.5.0 |
|
| >= 2.10.0 | >= v0.5.0 |
|
||||||
| < 2.10.0 | =< v0.4.10 |
|
| < 2.10.0 | =< v0.4.10 |
|
||||||
@@ -55,20 +56,21 @@ Example:
|
|||||||
```
|
```
|
||||||
PLUGINS_CONFIG = {
|
PLUGINS_CONFIG = {
|
||||||
'netbox_topology_views': {
|
'netbox_topology_views': {
|
||||||
'device_img': 'router,switch,firewall',
|
'device_img': ['router','switch', 'firewall'],
|
||||||
'preselected_device_roles': 'Router, Firewall'
|
'preselected_device_roles': ['Router', 'Firewall']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Setting | Default value | Description |
|
| Setting | Default value | Description |
|
||||||
| ------------- |-------------| -----|
|
| ------------- |-------------| -----|
|
||||||
| device_img | 'access-switch,core-switch,firewall,router,distribution-switch,backup,storage,wan-network,wireless-ap,server,internal-switch,isp-cpe-material,non-racked-devices,power-units' | The slug of the device roles that you have a image for. |
|
| device_img |['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage,wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'] | The slug of the device roles that you have a image for. |
|
||||||
| preselected_device_roles | 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP' | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
|
| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
|
||||||
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates |
|
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates |
|
||||||
| ignore_cable_type | 'power outlet,power port' | The cable types that you want to ignore in the views |
|
| ignore_cable_type | ['power outlet', 'power port'] | The cable types that you want to ignore in the views |
|
||||||
| preselected_tags | '' | The name of tags you want to preload |
|
| preselected_tags | '[]' | The name of tags you want to preload |
|
||||||
| enable_circuit_terminations | False | (bool) Set to true if you want to see circuit terminations in the topology |
|
| enable_circuit_terminations | False | (bool) Set to true if you want to see circuit terminations in the topology |
|
||||||
|
| draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) |
|
||||||
|
|
||||||
### Custom Images
|
### Custom Images
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ 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 = '0.5.3'
|
version = '1.0.0-alpha.2'
|
||||||
author = 'Mattijs Vanhaverbeke'
|
author = 'Mattijs Vanhaverbeke'
|
||||||
author_email = 'author@example.com'
|
author_email = 'author@example.com'
|
||||||
base_url = 'topology-views'
|
base_url = 'netbox_topology_views'
|
||||||
required_settings = []
|
required_settings = []
|
||||||
default_settings = {
|
default_settings = {
|
||||||
'preselected_device_roles': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
|
'preselected_device_roles': ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'],
|
||||||
'ignore_cable_type': 'power outlet,power port',
|
'ignore_cable_type': ['power outlet','power port'],
|
||||||
'device_img': 'access-switch,core-switch,firewall,router,distribution-switch,backup,storage,wan-network,wireless-ap,server,internal-switch,isp-cpe-material,non-racked-devices,power-units',
|
'device_img': ['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage,wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'],
|
||||||
'allow_coordinates_saving': False,
|
'allow_coordinates_saving': False,
|
||||||
'preselected_tags' : '',
|
'preselected_tags' : [],
|
||||||
'enable_circuit_terminations': False
|
'enable_circuit_terminations': False,
|
||||||
|
'draw_default_layout': False
|
||||||
}
|
}
|
||||||
|
|
||||||
config = TopologyViewsConfig
|
config = TopologyViewsConfig
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from netbox.api import OrderedDefaultRouter
|
from rest_framework import routers
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
router = OrderedDefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.APIRootView = views.TopologyViewsRootView
|
|
||||||
|
|
||||||
|
|
||||||
router.register('preselectdeviceroles', views.PreSelectDeviceRolesViewSet)
|
router.register('preselectdeviceroles', views.PreSelectDeviceRolesViewSet)
|
||||||
router.register('preselecttags', views.PreSelectTagsViewSet)
|
router.register('preselecttags', views.PreSelectTagsViewSet)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import django_filters
|
||||||
|
from extras.filters import TagFilter
|
||||||
|
from dcim.models import Device, DeviceRole, Region, Site, Location
|
||||||
|
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
class DeviceFilterSet(django_filters.FilterSet):
|
||||||
|
q = django_filters.CharFilter(
|
||||||
|
method='search',
|
||||||
|
label='Search',
|
||||||
|
)
|
||||||
|
tag = TagFilter()
|
||||||
|
|
||||||
|
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='device_role_id',
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
label='Role (ID)',
|
||||||
|
)
|
||||||
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
field_name='site__region',
|
||||||
|
lookup_expr='in',
|
||||||
|
label='Region (ID)',
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site (ID)',
|
||||||
|
)
|
||||||
|
location_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
field_name='location',
|
||||||
|
lookup_expr='in',
|
||||||
|
label='Location (ID)',
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Device
|
||||||
|
fields = ['id', 'name', ]
|
||||||
|
|
||||||
|
def search(self, queryset, name, value):
|
||||||
|
"""Perform the filtered search."""
|
||||||
|
if not value.strip():
|
||||||
|
return queryset
|
||||||
|
qs_filter = (
|
||||||
|
Q(name__icontains=value)
|
||||||
|
)
|
||||||
|
return queryset.filter(qs_filter)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from extras.models import Tag
|
||||||
|
from dcim.models import Device, Site, Region, DeviceRole, Location
|
||||||
|
from extras.forms import CustomFieldModelFilterForm
|
||||||
|
|
||||||
|
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, BootstrapMixin)
|
||||||
|
|
||||||
|
class DeviceFilterForm(CustomFieldModelFilterForm):
|
||||||
|
model = Device
|
||||||
|
field_groups = [
|
||||||
|
['q'],
|
||||||
|
['region_id', 'site_id', 'location_id'],
|
||||||
|
['device_role_id'],
|
||||||
|
['tag'],
|
||||||
|
]
|
||||||
|
|
||||||
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Region')
|
||||||
|
)
|
||||||
|
device_role_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=DeviceRole.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Device Role')
|
||||||
|
)
|
||||||
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region_id',
|
||||||
|
},
|
||||||
|
label=_('Site')
|
||||||
|
)
|
||||||
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region_id',
|
||||||
|
'site_id': '$site_id',
|
||||||
|
},
|
||||||
|
label=_('Location')
|
||||||
|
)
|
||||||
|
|
||||||
|
tag = TagFilterField(model)
|
||||||
@@ -5,6 +5,13 @@ menu_items = (
|
|||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link='plugins:netbox_topology_views:home',
|
link='plugins:netbox_topology_views:home',
|
||||||
link_text='Topology',
|
link_text='Topology',
|
||||||
buttons=()
|
buttons=(
|
||||||
|
PluginMenuButton(
|
||||||
|
link='plugins:netbox_topology_views:home',
|
||||||
|
title='Topology View',
|
||||||
|
icon_class='mdi mdi-plus-thick',
|
||||||
|
permissions=[],
|
||||||
|
),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
|
|||||||
#visgraph {
|
#visgraph {
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,343 +35,40 @@ var options = {
|
|||||||
var selected_regions = [];
|
var selected_regions = [];
|
||||||
var selected_sites = [];
|
var selected_sites = [];
|
||||||
|
|
||||||
|
function addEdge(item) {
|
||||||
|
edges.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNode(item) {
|
||||||
|
nodes.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
function iniPlotboxIndex() {
|
function iniPlotboxIndex() {
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
container = document.getElementById('visgraph');
|
container = document.getElementById('visgraph');
|
||||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
handleLoadData();
|
||||||
startLoadSearchBar();
|
|
||||||
handleButtonPress();
|
|
||||||
downloadButton = document.getElementById('btnDownloadImage');
|
downloadButton = document.getElementById('btnDownloadImage');
|
||||||
btnFullView = document.getElementById('btnFullView');
|
btnFullView = document.getElementById('btnFullView');
|
||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLoadSearchBar() {
|
function handleLoadData() {
|
||||||
$('#device-roles').select2({
|
|
||||||
allowClear: true,
|
|
||||||
placeholder: "---------",
|
|
||||||
theme: "bootstrap",
|
|
||||||
multiple: true,
|
|
||||||
ajax: {
|
|
||||||
url: "../../api/dcim/device-roles/?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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#sites').select2({
|
|
||||||
allowClear: true,
|
|
||||||
placeholder: "---------",
|
|
||||||
theme: "bootstrap",
|
|
||||||
multiple: true,
|
|
||||||
ajax: {
|
|
||||||
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 = "®ion_id=" + selected_regions[i];
|
|
||||||
base_url = base_url + tmp;
|
|
||||||
}
|
|
||||||
return base_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
graph = null;
|
||||||
dataType: "json",
|
nodes = new vis.DataSet();
|
||||||
type: "GET",
|
edges = new vis.DataSet();
|
||||||
data: function (params) {
|
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||||
var queryParameters = {
|
|
||||||
q: params.term
|
|
||||||
}
|
|
||||||
return queryParameters;
|
|
||||||
},
|
|
||||||
processResults: function (data) {
|
|
||||||
return {
|
|
||||||
results: $.map(data.results, function (item) {
|
|
||||||
return {
|
|
||||||
text: item.name,
|
|
||||||
id: item.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#sites').on('select2:select', function (e) {
|
topology_data.edges.forEach(addEdge);
|
||||||
var data = e.params.data;
|
topology_data.nodes.forEach(addNode);
|
||||||
var index = selected_sites.indexOf(data.id.toString());
|
|
||||||
if (index == -1) {
|
|
||||||
selected_sites.push(data.id.toString());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#sites').on('select2:unselect', function (e) {
|
|
||||||
var data = e.params.data;
|
|
||||||
var index = selected_sites.indexOf(data.id.toString());
|
|
||||||
if (index > -1) {
|
|
||||||
selected_sites.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#sites').on('select2:clear', function (e) {
|
|
||||||
selected_sites = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#tags').select2({
|
graph.fit();
|
||||||
allowClear: true,
|
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
|
||||||
placeholder: "---------",
|
|
||||||
theme: "bootstrap",
|
|
||||||
multiple: true,
|
|
||||||
ajax: {
|
|
||||||
url: "../../api/extras/tags/?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').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) {
|
graph.on('afterDrawing', function () {
|
||||||
var data = e.params.data;
|
var image = canvas.toDataURL(MIME_TYPE);
|
||||||
var index = selected_regions.indexOf(data.id.toString());
|
downloadButton.href = image;
|
||||||
if (index == -1) {
|
downloadButton.download = "topology";
|
||||||
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());
|
|
||||||
if (index > -1) {
|
|
||||||
selected_regions.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#regions').on('select2:clear', function (e) {
|
|
||||||
selected_regions = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#locations').select2({
|
|
||||||
allowClear: true,
|
|
||||||
placeholder: "---------",
|
|
||||||
theme: "bootstrap",
|
|
||||||
multiple: true,
|
|
||||||
ajax: {
|
|
||||||
url: function (params) {
|
|
||||||
var base_url = "../../api/dcim/locations/?brief=true";
|
|
||||||
if (selected_sites.length == 0) {
|
|
||||||
return base_url;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var i = 0; i < selected_sites.length; i++) {
|
|
||||||
var tmp = "&site_id=" + selected_sites[i];
|
|
||||||
base_url = base_url + tmp;
|
|
||||||
}
|
|
||||||
return base_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var deviceRolesSelect = $('#device-roles');
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: '../../api/plugins/topology-views/preselectdeviceroles/'
|
|
||||||
}).then(function (data) {
|
|
||||||
$.each(data.results, function (index, device_role_to_preload) {
|
|
||||||
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
|
|
||||||
deviceRolesSelect.append(option).trigger('change');
|
|
||||||
deviceRolesSelect.trigger({
|
|
||||||
type: 'select2:select',
|
|
||||||
params: {
|
|
||||||
data: device_role_to_preload
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
var tagsSelect = $('#tags');
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: '../../api/plugins/topology-views/preselecttags/'
|
|
||||||
}).then(function (data) {
|
|
||||||
$.each(data.results, function (index, tags_to_preload) {
|
|
||||||
var option = new Option(tags_to_preload.name, tags_to_preload.id, true, true);
|
|
||||||
tagsSelect.append(option).trigger('change');
|
|
||||||
tagsSelect.trigger({
|
|
||||||
type: 'select2:select',
|
|
||||||
params: {
|
|
||||||
data: tags_to_preload
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleButtonPress() {
|
|
||||||
$("#search-form").submit(function (event) {
|
|
||||||
$("#status").html('<span class="label label-info">Loading data</span>');
|
|
||||||
event.preventDefault();
|
|
||||||
var value = $("#name").val();
|
|
||||||
var value2 = $("#device-roles").val();
|
|
||||||
var value3 = $("#sites").val();
|
|
||||||
var value4 = $("#tags").val();
|
|
||||||
var value5 = $("#regions").val();
|
|
||||||
var value6 = $('#checkHideUnconnected').is(":checked");
|
|
||||||
var value7 = $("#locations").val();
|
|
||||||
$.ajax({
|
|
||||||
type: "GET",
|
|
||||||
url: "../../api/plugins/topology-views/search/search/",
|
|
||||||
data: {
|
|
||||||
'name': value,
|
|
||||||
'devicerole[]': value2,
|
|
||||||
'sites[]': value3,
|
|
||||||
'tags[]': value4,
|
|
||||||
'regions[]': value5,
|
|
||||||
'hide_unconnected': value6,
|
|
||||||
'locations[]': value7
|
|
||||||
},
|
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
dataType: "json",
|
|
||||||
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>');
|
|
||||||
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('visgraph').getElementsByTagName('canvas')[0];
|
|
||||||
|
|
||||||
graph.on('afterDrawing', function () {
|
|
||||||
$("#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) {
|
|
||||||
dragged = this.getPositions(params.nodes);
|
|
||||||
$.each(dragged, function (node_id, coordinates) {
|
|
||||||
$("#coordstatus").html('');
|
|
||||||
if ($('#checkSaveCoordinates').is(":checked")) {
|
|
||||||
//nodes.update({ id: node_id, physics: false });
|
|
||||||
$.ajax({
|
|
||||||
url: "../../api/plugins/topology-views/save-coords/save_coords/",
|
|
||||||
type: 'PATCH',
|
|
||||||
dataType: 'json',
|
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
processData: false,
|
|
||||||
data: JSON.stringify({
|
|
||||||
'node_id': node_id,
|
|
||||||
'x': coordinates.x,
|
|
||||||
'y': coordinates.y
|
|
||||||
}),
|
|
||||||
error: function (error_result) {
|
|
||||||
$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>');
|
|
||||||
},
|
|
||||||
success: function (data_result) {
|
|
||||||
$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
|
||||||
error: function (error_result) {
|
|
||||||
$("#status").html('<span class="label label-warning">Something went wrong</span>');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +1,78 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load perms %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block title %}Topology Views{% endblock %}
|
||||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
|
||||||
|
{% block head %}
|
||||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div class="row ">
|
{% block controls %}
|
||||||
<div class="col-md-9">
|
<div class="controls">
|
||||||
<div class="panel panel-default">
|
<div class="control-group">
|
||||||
<div class="panel-heading">
|
{% block extra_controls %}{% endblock %}
|
||||||
<strong>Network</strong> <span id="status"></span>
|
<a id="btnDownloadImage" class="btn btn-sm btn-info">
|
||||||
<div class="buttons pull-right">
|
<i class="mdi mdi-download"></i>
|
||||||
<a id="btnDownloadImage" class="btn btn-xs btn-info">
|
Download
|
||||||
<i class="mdi mdi-download"></i>
|
</a>
|
||||||
</a>
|
<a id="btnFullView" class="btn btn-sm btn-info disabled" target="_blank">
|
||||||
<a id="btnFullView" class="btn btn-xs btn-info disabled" target="_blank">
|
<i class="mdi mdi-share"></i>
|
||||||
<i class="mdi mdi-share"></i>
|
Share
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock controls %}
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
<ul class="nav nav-tabs px-3">
|
||||||
|
{% block tab_items %}
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
|
||||||
|
Network
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% if filter_form %}
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||||
|
Filters
|
||||||
|
{% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock tab_items %}
|
||||||
|
</ul>
|
||||||
|
{% endblock tabs %}
|
||||||
|
|
||||||
|
{% block content-wrapper %}
|
||||||
|
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
|
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div id="visgraph" class=""></div>
|
<div id="visgraph" class=""></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var topology_data = {{ topology_data | safe }};
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if filter_form %}
|
||||||
|
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||||
|
{% include 'inc/filter_list.html' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Search</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<form id="search-form" class="form" autocomplete="off">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<input id="name" class="form-control" type="text">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="device-roles">Device roles</label>
|
|
||||||
<select class="form-control" multiple="multiple" id="device-roles"></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="regions">Regions</label>
|
|
||||||
<select class="form-control" multiple="multiple" id="regions"></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="sites">Sites</label>
|
|
||||||
<select class="form-control" multiple="multiple" id="sites"></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="locations">Locations</label>
|
|
||||||
<select class="form-control" multiple="multiple" id="locations"></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="tags">Tags</label>
|
|
||||||
<select class="form-control" multiple="multiple" id="tags"></select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="custom-control custom-checkbox">
|
|
||||||
<input type="checkbox" class="custom-control-input" id="checkHideUnconnected">
|
|
||||||
<label class="custom-control-label" for="checkHideUnconnected">Hide Unconnected</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class="mb-4">
|
|
||||||
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||||
|
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||||
{% if config.allow_coordinates_saving == True %}
|
|
||||||
<div class="panel panel-default mt-3">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Settings</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="custom-control custom-checkbox">
|
|
||||||
<input type="checkbox" class="custom-control-input" id="checkSaveCoordinates">
|
|
||||||
<label class="custom-control-label" for="checkSaveCoordinates">Save coordinates</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span id="coordstatus"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<script type="application/javascript"> iniPlotboxIndex()</script>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock content-wrapper %}
|
||||||
|
|
||||||
{% block javascript %}
|
|
||||||
|
|
||||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
|
||||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
|
||||||
|
|
||||||
<script type="application/javascript"> iniPlotboxIndex()</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ 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'),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,155 @@
|
|||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.db.models import Q
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import QueryDict
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
|
||||||
|
from .forms import DeviceFilterForm
|
||||||
|
from .filters import DeviceFilterSet
|
||||||
|
|
||||||
|
import json
|
||||||
|
import distutils
|
||||||
|
|
||||||
|
from dcim.models import Device, Cable, DeviceRole, DeviceType
|
||||||
|
from extras.models import Tag
|
||||||
|
|
||||||
|
def get_topology_data(queryset):
|
||||||
|
nodes = []
|
||||||
|
nodes_ids = []
|
||||||
|
edges = []
|
||||||
|
edge_ids = 0
|
||||||
|
cable_ids = []
|
||||||
|
circuit_ids = []
|
||||||
|
if not queryset:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for qs_device in queryset:
|
||||||
|
device_has_connections = False
|
||||||
|
links_from_device = Cable.objects.filter(Q(_termination_a_device_id=qs_device.id) | Q(_termination_b_device_id=qs_device.id) )
|
||||||
|
for link_from in links_from_device:
|
||||||
|
device_has_connections = True
|
||||||
|
if link_from.termination_a_type.name != "circuit termination" and link_from.termination_b_type.name != "circuit termination":
|
||||||
|
if link_from.id not in cable_ids:
|
||||||
|
cable_ids.append(link_from.id)
|
||||||
|
edge_ids += 1
|
||||||
|
cable_a_dev_name = link_from.termination_a.device.name
|
||||||
|
if cable_a_dev_name is None:
|
||||||
|
cable_a_dev_name = "device A name unknown"
|
||||||
|
cable_a_name = link_from.termination_a.name
|
||||||
|
if cable_a_name is None:
|
||||||
|
cable_a_name = "cable A name unknown"
|
||||||
|
cable_b_dev_name = link_from.termination_b.device.name
|
||||||
|
if cable_b_dev_name is None:
|
||||||
|
cable_b_dev_name = "device B name unknown"
|
||||||
|
cable_b_name = link_from.termination_b.name
|
||||||
|
if cable_b_name is None:
|
||||||
|
cable_b_name = "cable B name unknown"
|
||||||
|
|
||||||
|
edge = {}
|
||||||
|
edge["id"] = edge_ids
|
||||||
|
edge["from"] = link_from.termination_a.device.id
|
||||||
|
edge["to"] = link_from.termination_b.device.id
|
||||||
|
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
||||||
|
if link_from.color != "":
|
||||||
|
edge["color"] = "#" + link_from.color
|
||||||
|
edges.append(edge)
|
||||||
|
else:
|
||||||
|
if link_from.termination_a.circuit.id not in circuit_ids:
|
||||||
|
circuit_ids.append(link_from.termination_a.circuit.id)
|
||||||
|
edge_ids += 1
|
||||||
|
|
||||||
|
cable_b_dev_name = link_from.termination_b.device.name
|
||||||
|
if cable_b_dev_name is None:
|
||||||
|
cable_b_dev_name = "device B name unknown"
|
||||||
|
cable_b_name = link_from.termination_b.name
|
||||||
|
if cable_b_name is None:
|
||||||
|
cable_b_name = "cable B name unknown"
|
||||||
|
|
||||||
|
edge = {}
|
||||||
|
edge["id"] = edge_ids
|
||||||
|
edge["to"] = link_from.termination_b.device.id
|
||||||
|
edge["dashes"] = True
|
||||||
|
title = ""
|
||||||
|
|
||||||
|
title += "Circuit provider: " + link_from.termination_a.circuit.provider.name + "<br>"
|
||||||
|
title += "Termination between <br>"
|
||||||
|
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
|
||||||
|
|
||||||
|
if link_from.termination_a.circuit.termination_a is not None and link_from.termination_a.circuit.termination_a.cable is not None and link_from.termination_a.circuit.termination_a.cable.id != link_from.id and link_from.termination_a.circuit.termination_a.cable.termination_b is not None and link_from.termination_a.circuit.termination_a.cable.termination_b.device is not None:
|
||||||
|
edge["from"] = link_from.termination_a.circuit.termination_a.cable.termination_b.device.id
|
||||||
|
|
||||||
|
cable_a_dev_name = link_from.termination_a.circuit.termination_a.cable.termination_b.device.name
|
||||||
|
if cable_a_dev_name is None:
|
||||||
|
cable_a_dev_name = "device B name unknown"
|
||||||
|
cable_b_name = link_from.termination_a.circuit.termination_a.cable.termination_b.name
|
||||||
|
if cable_a_name is None:
|
||||||
|
cable_a_name = "cable B name unknown"
|
||||||
|
title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
|
||||||
|
edge["title"] = title
|
||||||
|
edges.append(edge)
|
||||||
|
|
||||||
|
if link_from.termination_a.circuit.termination_z is not None and link_from.termination_a.circuit.termination_z.cable is not None and link_from.termination_a.circuit.termination_z.cable.id != link_from.id and link_from.termination_a.circuit.termination_z.cable.termination_b is not None and link_from.termination_a.circuit.termination_z.cable.termination_b.device is not None:
|
||||||
|
edge["from"] = link_from.termination_a.circuit.termination_z.cable.termination_b.device.id
|
||||||
|
|
||||||
|
cable_a_dev_name = link_from.termination_a.circuit.termination_z.cable.termination_b.device.name
|
||||||
|
if cable_a_dev_name is None:
|
||||||
|
cable_a_dev_name = "device B name unknown"
|
||||||
|
cable_a_name = link_from.termination_a.circuit.termination_z.cable.termination_b.name
|
||||||
|
if cable_a_name is None:
|
||||||
|
cable_a_name = "cable B name unknown"
|
||||||
|
title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
|
||||||
|
edge["title"] = title
|
||||||
|
edges.append(edge)
|
||||||
|
|
||||||
|
if qs_device.id not in nodes_ids:
|
||||||
|
nodes_ids.append(qs_device.id)
|
||||||
|
|
||||||
|
dev_name = qs_device.name
|
||||||
|
if dev_name is None:
|
||||||
|
dev_name = "device name unknown"
|
||||||
|
|
||||||
|
node_content = ""
|
||||||
|
|
||||||
|
if qs_device.device_type is not None:
|
||||||
|
node_content += "<tr><th>Type: </th><td>" + qs_device.device_type.model + "</td></tr>"
|
||||||
|
if qs_device.device_role.name is not None:
|
||||||
|
node_content += "<tr><th>Role: </th><td>" + qs_device.device_role.name + "</td></tr>"
|
||||||
|
if qs_device.serial != "":
|
||||||
|
node_content += "<tr><th>Serial: </th><td>" + qs_device.serial + "</td></tr>"
|
||||||
|
if qs_device.primary_ip is not None:
|
||||||
|
node_content += "<tr><th>IP Address: </th><td>" + str(qs_device.primary_ip.address) + "</td></tr>"
|
||||||
|
|
||||||
|
dev_title = "<table> %s </table>" % (node_content)
|
||||||
|
|
||||||
|
node = {}
|
||||||
|
node["id"] = qs_device.id
|
||||||
|
node["name"] = dev_name
|
||||||
|
node["label"] = dev_name
|
||||||
|
node["title"] = dev_title
|
||||||
|
node["shape"] = 'image'
|
||||||
|
if qs_device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
|
||||||
|
node["image"] = '../../static/netbox_topology_views/img/' + qs_device.device_role.slug + ".png"
|
||||||
|
else:
|
||||||
|
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
|
||||||
|
|
||||||
|
if qs_device.device_role.color != "":
|
||||||
|
node["color.border"] = "#" + qs_device.device_role.color
|
||||||
|
|
||||||
|
if "coordinates" in qs_device.custom_field_data:
|
||||||
|
if qs_device.custom_field_data["coordinates"] is not None:
|
||||||
|
cords = qs_device.custom_field_data["coordinates"].split(";")
|
||||||
|
node["x"] = int(cords[0])
|
||||||
|
node["y"] = int(cords[1])
|
||||||
|
node["physics"] = False
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
results["nodes"] = nodes
|
||||||
|
results["edges"] = edges
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
class TopologyHomeView(PermissionRequiredMixin, View):
|
class TopologyHomeView(PermissionRequiredMixin, View):
|
||||||
permission_required = ('dcim.view_site', 'dcim.view_device')
|
permission_required = ('dcim.view_site', 'dcim.view_device')
|
||||||
@@ -9,13 +158,34 @@ 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')
|
self.filterset = DeviceFilterSet
|
||||||
|
self.queryset = Device.objects.all()
|
||||||
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
|
topo_data = None
|
||||||
|
|
||||||
class TopologyFullView(PermissionRequiredMixin, View):
|
if request.GET:
|
||||||
permission_required = ('dcim.view_site', 'dcim.view_device')
|
if 'draw_init' in request.GET:
|
||||||
|
if bool(distutils.util.strtobool(request.GET["draw_init"])):
|
||||||
|
topo_data = get_topology_data(self.queryset)
|
||||||
|
else:
|
||||||
|
topo_data = get_topology_data(self.queryset)
|
||||||
|
else:
|
||||||
|
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
||||||
|
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
|
||||||
|
|
||||||
"""
|
q_device_role_id = DeviceRole.objects.filter(name__in=preselected_device_roles).values_list('id', flat=True)
|
||||||
Show the full view page
|
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list('name', flat=True)
|
||||||
"""
|
|
||||||
def get(self, request):
|
q = QueryDict(mutable=True)
|
||||||
return render(request, 'netbox_topology_views/full.html')
|
q.setlist('device_role_id', list(q_device_role_id))
|
||||||
|
q.setlist('tag', list(q_tags))
|
||||||
|
q['draw_init'] = settings.PLUGINS_CONFIG["netbox_topology_views"]["draw_default_layout"]
|
||||||
|
query_string = q.urlencode()
|
||||||
|
print(query_string)
|
||||||
|
return HttpResponseRedirect(request.path + "?" + query_string)
|
||||||
|
|
||||||
|
return render(request, 'netbox_topology_views/index.html' , {
|
||||||
|
'filter_form': DeviceFilterForm(request.GET, label_suffix=''),
|
||||||
|
'topology_data': json.dumps(topo_data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "0.5.3",
|
"version": "1.0.0-alpha.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "0.5.3",
|
"version": "1.0.0-alpha.2",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"resources": "gulp build",
|
"resources": "gulp build",
|
||||||
"resources_dev": "gulp build_dev"
|
"resources_dev": "gulp build_dev"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='netbox-topology-views',
|
name='netbox-topology-views',
|
||||||
version='0.5.3',
|
version='1.0.0-alpha.2',
|
||||||
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',
|
||||||
|
|||||||
Reference in New Issue
Block a user