Compare commits

..
24 changed files with 530 additions and 324 deletions
+3 -4
View File
@@ -8,7 +8,7 @@ Support to filter on name, site, tag and device role.
## Preview ## Preview
![preview image](doc/img/preview_3.1.jpeg?raw=true "preview") ![preview image](doc/img/preview.png?raw=true "preview")
## Install ## Install
@@ -35,8 +35,7 @@ Then run `python3 manage.py collectstatic --no-input`
| netbox version | netbox-topology-views version | | netbox version | netbox-topology-views version |
| ------------- |-------------| | ------------- |-------------|
| >= 3.2.0 | >= v1.1.0 | | >= 3.1.8 | >= v1.0.0a1 |
| >= 3.1.8 | >= v1.0.0 |
| >= 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 |
@@ -81,7 +80,7 @@ If you add your own image you also need to add the slug to the `device_img` sett
## Use ## Use
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/topology-views/` to view your topologies
### Update ### Update
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

+2 -2
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 topology maps' description = 'An plugin to render topology maps'
version = '1.2.0' version = '1.0.0-alpha.2'
author = 'Mattijs Vanhaverbeke' author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com' author_email = 'author@example.com'
base_url = 'netbox_topology_views' base_url = 'netbox_topology_views'
@@ -12,7 +12,7 @@ class TopologyViewsConfig(PluginConfig):
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,
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
#no models yet
+12
View File
@@ -3,6 +3,18 @@ from rest_framework.serializers import ModelSerializer
from dcim.models import DeviceRole, Device from dcim.models import DeviceRole, Device
from extras.models import Tag from extras.models import Tag
class PreDeviceRoleSerializer(ModelSerializer):
class Meta:
model = DeviceRole
fields = ('id', 'name')
class PreTagSerializer(ModelSerializer):
class Meta:
model = Tag
fields = ('id', 'name')
class TopologyDummySerializer(ModelSerializer): class TopologyDummySerializer(ModelSerializer):
+3
View File
@@ -3,6 +3,9 @@ from . import views
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register('preselectdeviceroles', views.PreSelectDeviceRolesViewSet)
router.register('preselecttags', views.PreSelectTagsViewSet)
router.register('search', views.SearchViewSet, basename='search')
router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords') router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords')
urlpatterns = router.urls urlpatterns = router.urls
+214 -1
View File
@@ -4,7 +4,7 @@ from rest_framework.response import Response
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from .serializers import TopologyDummySerializer from .serializers import PreDeviceRoleSerializer, TopologyDummySerializer, PreTagSerializer
from django.conf import settings from django.conf import settings
from dcim.models import DeviceRole, Device, Cable from dcim.models import DeviceRole, Device, Cable
@@ -12,10 +12,26 @@ from circuits.models import Circuit
from extras.models import Tag from extras.models import Tag
ignore_cable_type_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
ignore_cable_type = ignore_cable_type_raw.split(",")
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_device_roles = preselected_device_roles_raw.split(",")
preselected_tags_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
preselected_tags = preselected_tags_raw.split(",")
class TopologyViewsRootView(APIRootView): class TopologyViewsRootView(APIRootView):
def get_view_name(self): def get_view_name(self):
return 'TopologyViews' return 'TopologyViews'
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
serializer_class = PreDeviceRoleSerializer
class PreSelectTagsViewSet(ReadOnlyModelViewSet):
queryset = Tag.objects.filter(name__in=preselected_tags)
serializer_class = PreTagSerializer
class SaveCoordsViewSet(ReadOnlyModelViewSet): class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.all() queryset = Device.objects.all()
@@ -58,3 +74,200 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
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(ReadOnlyModelViewSet):
queryset = Device.objects.all()
serializer_class = TopologyDummySerializer
def _filter(self, site, role, name, tags, region, location):
filter_devices = Device.objects.all()
if name is not None:
filter_devices = filter_devices.filter(name__contains=name)
if site is not None:
filter_devices = filter_devices.filter(site__id__in=site)
if role is not None:
filter_devices = filter_devices.filter(device_role__id__in=role)
if tags is not None:
filter_devices = filter_devices.filter(tags__id__in=tags)
if region is not None:
filter_devices = filter_devices.filter(site__region__id__in=region)
if location is not None:
filter_devices = filter_devices.filter(location__id__in=location)
return filter_devices
@action(detail=False, methods=['get'])
def search(self, request):
name = request.query_params.get('name', None)
if name == "":
name = None
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
locations = request.query_params.getlist('locations[]', None)
if locations == []:
locations = None
hide_unconnected = request.query_params.get('hide_unconnected', None)
if hide_unconnected == "":
hide_unconnected = None
devices = self._filter(sites, devicerole, name, tags, regions, locations)
nodes = []
edges = []
edge_ids = 0
cable_ids = []
circuit_ids = []
for device in devices:
cables = device.get_cables()
if cables.exists():
device_has_connections = True
for cable in cables:
if cable.termination_a_type.name != "circuit termination" and cable.termination_b_type.name != "circuit termination":
if cable.id not in cable_ids:
if cable.termination_a_type.name not in ignore_cable_type and cable.termination_b_type.name not in ignore_cable_type:
cable_ids.append(cable.id)
edge_ids += 1
cable_a_dev_name = cable.termination_a.device.name
if cable_a_dev_name is None:
cable_a_dev_name = "device A name unknown"
cable_a_name = cable.termination_a.name
if cable_a_name is None:
cable_a_name = "cable A name unknown"
cable_b_dev_name = cable.termination_b.device.name
if cable_b_dev_name is None:
cable_b_dev_name = "device B name unknown"
cable_b_name = cable.termination_b.name
if cable_b_name is None:
cable_b_name = "cable B name unknown"
edge = {}
edge["id"] = edge_ids
edge["from"] = cable.termination_a.device.id
edge["to"] = cable.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 cable.color != "":
edge["color"] = "#" + cable.color
edges.append(edge)
else:
if cable.termination_a_type.name == "circuit termination":
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]:
if cable.termination_a.circuit.id not in circuit_ids:
circuit_ids.append(cable.termination_a.circuit.id)
edge_ids += 1
cable_b_dev_name = cable.termination_b.device.name
if cable_b_dev_name is None:
cable_b_dev_name = "device B name unknown"
cable_b_name = cable.termination_b.name
if cable_b_name is None:
cable_b_name = "cable B name unknown"
edge = {}
edge["id"] = edge_ids
edge["to"] = cable.termination_b.device.id
edge["dashes"] = True
title = ""
title += "Circuit provider: " + cable.termination_a.circuit.provider.name + "<br>"
title += "Termination between <br>"
title += cable_b_dev_name + " [" + cable_b_name + "]<br>"
# To Many if's
if cable.termination_a.circuit.termination_a is not None:
if cable.termination_a.circuit.termination_a.cable is not None:
if cable.termination_a.circuit.termination_a.cable.id != cable.id:
if cable.termination_a.circuit.termination_a.cable.termination_b is not None:
if cable.termination_a.circuit.termination_a.cable.termination_b.device is not None:
edge["from"] = cable.termination_a.circuit.termination_a.cable.termination_b.device.id
cable_a_dev_name = cable.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 = cable.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)
# To Many if's
if cable.termination_a.circuit.termination_z is not None:
if cable.termination_a.circuit.termination_z.cable is not None:
if cable.termination_a.circuit.termination_z.cable.id != cable.id:
if cable.termination_a.circuit.termination_z.cable.termination_b is not None:
if cable.termination_a.circuit.termination_z.cable.termination_b.device is not None:
edge["from"] = cable.termination_a.circuit.termination_z.cable.termination_b.device.id
cable_a_dev_name = cable.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 = cable.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)
else:
device_has_connections = False
if hide_unconnected == 'false' or (hide_unconnected == 'true' and device_has_connections is True):
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
node_content = ""
if device.device_type.display_name is not None:
node_content += "<tr><th>Type: </th><td>" + device.device_type.display_name + "</td></tr>"
if device.device_role.name is not None:
node_content += "<tr><th>Role: </th><td>" + device.device_role.name + "</td></tr>"
if device.serial != "":
node_content += "<tr><th>Serial: </th><td>" + device.serial + "</td></tr>"
if device.primary_ip is not None:
node_content += "<tr><th>IP Address: </th><td>" + str(device.primary_ip.address) + "</td></tr>"
dev_title = "<table> %s </table>" % (node_content)
node = {}
node["id"] = device.id
node["name"] = dev_name
node["label"] = dev_name
node["title"] = dev_title
node["shape"] = 'image'
if device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
node["image"] = '../../static/netbox_topology_views/img/' + device.device_role.slug + ".png"
else:
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color
if "coordinates" in device.custom_field_data:
if device.custom_field_data["coordinates"] != "":
cords = 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 Response(results)
+8 -13
View File
@@ -1,21 +1,16 @@
import django_filters 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 from django.db.models import Q
from dcim.models import Device, DeviceRole, Region, Site, Location class DeviceFilterSet(django_filters.FilterSet):
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.models import TenantGroup, Tenant
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
) )
tag = TagFilter()
device_role_id = django_filters.ModelMultipleChoiceFilter( device_role_id = django_filters.ModelMultipleChoiceFilter(
field_name='device_role_id', field_name='device_role_id',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
@@ -40,7 +35,7 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = Device model = Device
fields = ['id', 'name'] fields = ['id', 'name', ]
def search(self, queryset, name, value): def search(self, queryset, name, value):
"""Perform the filtered search.""" """Perform the filtered search."""
@@ -49,4 +44,4 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
qs_filter = ( qs_filter = (
Q(name__icontains=value) Q(name__icontains=value)
) )
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
+10 -28
View File
@@ -1,30 +1,21 @@
import imp
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from extras.models import Tag
from dcim.models import Device, Site, Region, DeviceRole, Location from dcim.models import Device, Site, Region, DeviceRole, Location
from extras.forms import CustomFieldModelFilterForm
from django import forms from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, BootstrapMixin)
from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField)
allow_coordinates_saving = settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] class DeviceFilterForm(CustomFieldModelFilterForm):
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device model = Device
fieldsets = ( field_groups = [
(None, ('q', 'hide_unconnected', 'save_coords')), ['q'],
(None, ('tenant_group_id', 'tenant_id')), ['region_id', 'site_id', 'location_id'],
(None, ('region_id', 'site_id', 'location_id')), ['device_role_id'],
(None, ('device_role_id',)), ['tag'],
(None, ('tag',)), ]
)
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@@ -53,14 +44,5 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
}, },
label=_('Location') label=_('Location')
) )
hide_unconnected = forms.BooleanField(
label=_("Hide Unconnected"),
required=False,
initial=False)
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
required=False,
disabled=(not allow_coordinates_saving),
initial=False)
tag = TagFilterField(model) tag = TagFilterField(model)
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# no modeles yet
+10 -2
View File
@@ -4,6 +4,14 @@ from utilities.choices import ButtonColorChoices
menu_items = ( menu_items = (
PluginMenuItem( PluginMenuItem(
link='plugins:netbox_topology_views:home', link='plugins:netbox_topology_views:home',
link_text='Topology' link_text='Topology',
buttons=(
PluginMenuButton(
link='plugins:netbox_topology_views:home',
title='Topology View',
icon_class='mdi mdi-plus-thick',
permissions=[],
),
)
), ),
) )
@@ -1 +1 @@
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529} #visgraph{height:70vh}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
var graph=null,container=null,downloadButton=null,MIME_TYPE="image/png",canvas=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!1},nodes:{shape:"image",brokenImage:"../../static/netbox_topology_views/img/role-unknown.png",size:35,font:{multi:"md",face:"helvetica"}},edges:{length:100,width:2,font:{face:"helvetica"},shadow:{enabled:!0}},physics:{solver:"forceAtlas2Based"}},selected_regions=[],selected_sites=[],coord_save_checkbox=null,htmlElement=null;function getCookie(e){var t=null;if(document.cookie&&""!==document.cookie)for(var o=document.cookie.split(";"),n=0;n<o.length;n++){var d=o[n].trim();if(d.substring(0,e.length+1)===e+"="){t=decodeURIComponent(d.substring(e.length+1));break}}return t}function htmlTitle(e){return(container=document.createElement("div")).innerHTML=e,container}function addEdge(e){e.title=htmlTitle(e.title),edges.add(e)}function addNode(e){e.title=htmlTitle(e.title),nodes.add(e)}function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){csrftoken=getCookie("csrftoken"),container=document.getElementById("visgraph"),htmlElement=document.getElementsByTagName("html")[0],downloadButton=document.getElementById("btnDownloadImage"),handleLoadData(),btnFullView=document.getElementById("btnFullView"),coord_save_checkbox=document.getElementById("id_save_coords")},!1)}function performGraphDownload(){var e=document.createElement("a"),t=canvas.toDataURL(MIME_TYPE);e.href=t,e.download="topology",document.body.appendChild(e),e.click(),document.body.removeChild(e)}function handleLoadData(){null!==topology_data&&("dark"==htmlElement.dataset.netboxColorMode&&(options.nodes.font.color="#fff"),graph=null,nodes=new vis.DataSet,edges=new vis.DataSet,graph=new vis.Network(container,{nodes:nodes,edges:edges},options),topology_data.edges.forEach(addEdge),topology_data.nodes.forEach(addNode),graph.fit(),canvas=document.getElementById("visgraph").getElementsByTagName("canvas")[0],downloadButton.onclick=function(e){return performGraphDownload(),!1},graph.on("dragEnd",function(e){if(dragged=this.getPositions(e.nodes),coord_save_checkbox.checked&&0!==Object.keys(dragged).length)for(dragged_device in dragged){var t=dragged_device,o=new XMLHttpRequest;o.open("PATCH","/api/plugins/netbox_topology_views/save-coords/save_coords/"),o.setRequestHeader("X-CSRFToken",csrftoken),o.setRequestHeader("Accept","application/json"),o.setRequestHeader("Content-Type","application/json"),o.onreadystatechange=function(){4===o.readyState&&(console.log(o.status),console.log(o.responseText))};var n=JSON.stringify({node_id:t,x:dragged[t].x,y:dragged[t].y});o.send(n)}}))} var graph=null,container=null;function iniPlotboxFull(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("fullvisgraph"),startRender()},!1)}function startRender(){var e=location.search;$.ajax({type:"GET",url:"../../api/plugins/topology-views/search/search/"+e,contentType:"application/json; charset=utf-8",success:function(e,n,t){graph=null,nodes=new vis.DataSet,edges=new vis.DataSet,graph=new vis.Network(container,{nodes:nodes,edges:edges},options),$.each(e.nodes,function(e,n){nodes.add(n)}),$.each(e.edges,function(e,n){edges.add(n)}),graph.fit(),canvas=document.getElementById("fullvisgraph").getElementsByTagName("canvas")[0]}})}graph=null,container=null;var downloadButton=null,MIME_TYPE="image/png",canvas=null,csrftoken=null,nodes=new vis.DataSet,edges=new vis.DataSet,options={interaction:{hover:!0,hoverConnectedEdges:!0,multiselect:!1},nodes:{shape:"image",brokenImage:"../../static/netbox_topology_views/img/role-unknown.png",size:35,font:{multi:"md",face:"helvetica"}},edges:{length:100,width:2,font:{face:"helvetica"}},physics:{solver:"forceAtlas2Based"}},selected_regions=[],selected_sites=[];function addEdge(e){edges.add(e)}function addNode(e){nodes.add(e)}function iniPlotboxIndex(){document.addEventListener("DOMContentLoaded",function(){container=document.getElementById("visgraph"),handleLoadData(),downloadButton=document.getElementById("btnDownloadImage"),btnFullView=document.getElementById("btnFullView")},!1)}function handleLoadData(){graph=null,nodes=new vis.DataSet,edges=new vis.DataSet,graph=new vis.Network(container,{nodes:nodes,edges:edges},options),topology_data.edges.forEach(addEdge),topology_data.nodes.forEach(addNode),graph.fit(),canvas=document.getElementById("visgraph").getElementsByTagName("canvas")[0],graph.on("afterDrawing",function(){var e=canvas.toDataURL(MIME_TYPE);downloadButton.href=e,downloadButton.download="topology"})}
File diff suppressed because one or more lines are too long
@@ -2,6 +2,3 @@
height: 70vh; height: 70vh;
} }
html[data-netbox-color-mode=dark] #visgraph {
background-color: #212529;
}
@@ -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];
}
});
}
+6 -83
View File
@@ -27,9 +27,6 @@ var options = {
font: { font: {
face: 'helvetica', face: 'helvetica',
}, },
shadow: {
enabled: true
}
}, },
physics: { physics: {
solver: 'forceAtlas2Based' solver: 'forceAtlas2Based'
@@ -37,71 +34,25 @@ var options = {
}; };
var selected_regions = []; var selected_regions = [];
var selected_sites = []; var selected_sites = [];
var coord_save_checkbox = null;
var htmlElement = null;
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function htmlTitle(html) {
container = document.createElement("div");
container.innerHTML = html;
return container;
}
function addEdge(item) { function addEdge(item) {
item.title = htmlTitle( item.title );
edges.add(item); edges.add(item);
} }
function addNode(item) { function addNode(item) {
item.title = htmlTitle( item.title );
nodes.add(item); nodes.add(item);
} }
function iniPlotboxIndex() { function iniPlotboxIndex() {
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
csrftoken = getCookie('csrftoken');
container = document.getElementById('visgraph'); container = document.getElementById('visgraph');
htmlElement = document.getElementsByTagName("html")[0];
downloadButton = document.getElementById('btnDownloadImage');
handleLoadData(); handleLoadData();
downloadButton = document.getElementById('btnDownloadImage');
btnFullView = document.getElementById('btnFullView'); btnFullView = document.getElementById('btnFullView');
coord_save_checkbox = document.getElementById('id_save_coords');
}, false); }, false);
} }
function performGraphDownload() {
var tempDownloadLink = document.createElement('a');
var generatedImageUrl = canvas.toDataURL(MIME_TYPE);
tempDownloadLink.href = generatedImageUrl;
tempDownloadLink.download = "topology";
document.body.appendChild(tempDownloadLink);
tempDownloadLink.click();
document.body.removeChild(tempDownloadLink);
}
function handleLoadData() { function handleLoadData() {
if (topology_data !== null) {
if (htmlElement.dataset.netboxColorMode == "dark") {
options.nodes.font.color = "#fff";
}
graph = null; graph = null;
nodes = new vis.DataSet(); nodes = new vis.DataSet();
@@ -114,38 +65,10 @@ function handleLoadData() {
graph.fit(); graph.fit();
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0]; canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
downloadButton.onclick = function(e) { performGraphDownload(); return false; }; graph.on('afterDrawing', function () {
var image = canvas.toDataURL(MIME_TYPE);
graph.on("dragEnd", function (params) { downloadButton.href = image;
dragged = this.getPositions(params.nodes); downloadButton.download = "topology";
if (coord_save_checkbox.checked) {
if (Object.keys(dragged).length !== 0) {
for (dragged_device in dragged) {
var node_id = dragged_device;
var url = "/api/plugins/netbox_topology_views/save-coords/save_coords/";
var xhr = new XMLHttpRequest();
xhr.open("PATCH", url);
xhr.setRequestHeader('X-CSRFToken', csrftoken );
xhr.setRequestHeader("Accept", "application/json");
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log(xhr.status);
console.log(xhr.responseText);
}};
var data = JSON.stringify({
'node_id': node_id,
'x': dragged[node_id].x,
'y': dragged[node_id].y});
xhr.send(data);
}
}
}
}); });
}
} }
@@ -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.5.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 %}
@@ -16,10 +16,14 @@
<div class="controls"> <div class="controls">
<div class="control-group"> <div class="control-group">
{% block extra_controls %}{% endblock %} {% block extra_controls %}{% endblock %}
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#"> <a id="btnDownloadImage" class="btn btn-sm btn-info">
<i class="mdi mdi-download"></i> <i class="mdi mdi-download"></i>
Download Download
</a> </a>
<a id="btnFullView" class="btn btn-sm btn-info disabled" target="_blank">
<i class="mdi mdi-share"></i>
Share
</a>
</div> </div>
</div> </div>
{% endblock controls %} {% endblock controls %}
@@ -36,7 +40,7 @@
<li class="nav-item" role="presentation"> <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"> <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 Filters
{% if filter_form %}{% badge filter_form.changed_data|length bg_color="blue" %}{% endif %} {% if filter_form %}{% badge filter_form.changed_data|length bg_class="primary" %}{% endif %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
@@ -45,31 +49,30 @@
{% endblock tabs %} {% endblock tabs %}
{% block content-wrapper %} {% block content-wrapper %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %} {% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab"> <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"> <script type="text/javascript">
var topology_data = {{ topology_data | safe }}; var topology_data = {{ topology_data | safe }};
</script> </script>
</div> </div>
{% if filter_form %} {% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab"> <div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %} {% include 'inc/filter_list.html' %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endwith %}
{% endblock content-wrapper %}
{% block javascript %} <script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script> <script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
<script type="application/javascript">iniPlotboxIndex()</script>
{% endblock javascript %} <script type="application/javascript"> iniPlotboxIndex()</script>
{% endwith %}
{% endblock content-wrapper %}
+76 -93
View File
@@ -10,11 +10,12 @@ from .forms import DeviceFilterForm
from .filters import DeviceFilterSet from .filters import DeviceFilterSet
import json import json
import distutils
from dcim.models import Device, Cable, DeviceRole, DeviceType from dcim.models import Device, Cable, DeviceRole, DeviceType
from extras.models import Tag from extras.models import Tag
def get_topology_data(queryset, hide_unconnected): def get_topology_data(queryset):
nodes = [] nodes = []
nodes_ids = [] nodes_ids = []
edges = [] edges = []
@@ -24,98 +25,85 @@ def get_topology_data(queryset, hide_unconnected):
if not queryset: if not queryset:
return None return None
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
device_ids = [d.id for d in queryset]
for qs_device in queryset: for qs_device in queryset:
device_has_connections = False 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) )
links_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:
for link_from in links_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.termination_a_type.name != "circuit termination" and link_from.termination_b_type.name != "circuit termination":
if link_from.termination_a_type.name not in ignore_cable_type and link_from.termination_b_type.name not in ignore_cable_type: if link_from.id not in cable_ids:
if link_from.id not in cable_ids: cable_ids.append(link_from.id)
if link_from.termination_a.device.id in device_ids and link_from.termination_b.device.id in device_ids: edge_ids += 1
device_has_connections = True cable_a_dev_name = link_from.termination_a.device.name
cable_ids.append(link_from.id) if cable_a_dev_name is None:
edge_ids += 1 cable_a_dev_name = "device A name unknown"
cable_a_dev_name = link_from.termination_a.device.name cable_a_name = link_from.termination_a.name
if cable_a_dev_name is None: if cable_a_name is None:
cable_a_dev_name = "device A name unknown" cable_a_name = "cable A name unknown"
cable_a_name = link_from.termination_a.name cable_b_dev_name = link_from.termination_b.device.name
if cable_a_name is None: if cable_b_dev_name is None:
cable_a_name = "cable A name unknown" cable_b_dev_name = "device B name unknown"
cable_b_dev_name = link_from.termination_b.device.name cable_b_name = link_from.termination_b.name
if cable_b_dev_name is None: if cable_b_name is None:
cable_b_dev_name = "device B name unknown" cable_b_name = "cable 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 = {}
edge["id"] = edge_ids edge["id"] = edge_ids
edge["from"] = link_from.termination_a.device.id edge["from"] = link_from.termination_a.device.id
edge["to"] = link_from.termination_b.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 + "]" edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
if link_from.color != "": if link_from.color != "":
edge["color"] = "#" + link_from.color edge["color"] = "#" + link_from.color
edges.append(edge) edges.append(edge)
else:
if link_from.termination_a.device.id in device_ids and link_from.termination_b.device.id in device_ids:
device_has_connections = True
else: else:
if settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]: if link_from.termination_a.circuit.id not in circuit_ids:
if link_from.termination_a_type.name == "circuit termination": circuit_ids.append(link_from.termination_a.circuit.id)
if link_from.termination_a.circuit.id not in circuit_ids: edge_ids += 1
circuit_ids.append(link_from.termination_a.circuit.id)
edge_ids += 1
cable_b_dev_name = link_from.termination_b.device.name cable_b_dev_name = link_from.termination_b.device.name
if cable_b_dev_name is None: if cable_b_dev_name is None:
cable_b_dev_name = "device B name unknown" cable_b_dev_name = "device B name unknown"
cable_b_name = link_from.termination_b.name cable_b_name = link_from.termination_b.name
if cable_b_name is None: if cable_b_name is None:
cable_b_name = "cable B name unknown" cable_b_name = "cable B name unknown"
edge = {} edge = {}
edge["id"] = edge_ids edge["id"] = edge_ids
edge["to"] = link_from.termination_b.device.id edge["to"] = link_from.termination_b.device.id
edge["dashes"] = True edge["dashes"] = True
title = "" title = ""
title += "Circuit provider: " + link_from.termination_a.circuit.provider.name + "<br>" title += "Circuit provider: " + link_from.termination_a.circuit.provider.name + "<br>"
title += "Termination between <br>" title += "Termination between <br>"
title += cable_b_dev_name + " [" + cable_b_name + "]<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: 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 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 cable_a_dev_name = link_from.termination_a.circuit.termination_a.cable.termination_b.device.name
if cable_a_dev_name is None: if cable_a_dev_name is None:
cable_a_dev_name = "device B name unknown" cable_a_dev_name = "device B name unknown"
cable_b_name = link_from.termination_a.circuit.termination_a.cable.termination_b.name cable_b_name = link_from.termination_a.circuit.termination_a.cable.termination_b.name
if cable_a_name is None: if cable_a_name is None:
cable_a_name = "cable B name unknown" cable_a_name = "cable B name unknown"
title += cable_a_dev_name + " [" + cable_a_name + "]<br>" title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
edge["title"] = title edge["title"] = title
edges.append(edge) 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: 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 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 cable_a_dev_name = link_from.termination_a.circuit.termination_z.cable.termination_b.device.name
if cable_a_dev_name is None: if cable_a_dev_name is None:
cable_a_dev_name = "device B name unknown" cable_a_dev_name = "device B name unknown"
cable_a_name = link_from.termination_a.circuit.termination_z.cable.termination_b.name cable_a_name = link_from.termination_a.circuit.termination_z.cable.termination_b.name
if cable_a_name is None: if cable_a_name is None:
cable_a_name = "cable B name unknown" cable_a_name = "cable B name unknown"
title += cable_a_dev_name + " [" + cable_a_name + "]<br>" title += cable_a_dev_name + " [" + cable_a_name + "]<br>"
edge["title"] = title edge["title"] = title
edges.append(edge) edges.append(edge)
if qs_device.id not in nodes_ids: if qs_device.id not in nodes_ids:
if hide_unconnected == None or (hide_unconnected is True and device_has_connections is True):
nodes_ids.append(qs_device.id) nodes_ids.append(qs_device.id)
dev_name = qs_device.name dev_name = qs_device.name
@@ -151,11 +139,10 @@ def get_topology_data(queryset, hide_unconnected):
if "coordinates" in qs_device.custom_field_data: if "coordinates" in qs_device.custom_field_data:
if qs_device.custom_field_data["coordinates"] is not None: if qs_device.custom_field_data["coordinates"] is not None:
if ';' in qs_device.custom_field_data["coordinates"]: cords = qs_device.custom_field_data["coordinates"].split(";")
cords = qs_device.custom_field_data["coordinates"].split(";") node["x"] = int(cords[0])
node["x"] = int(cords[0]) node["y"] = int(cords[1])
node["y"] = int(cords[1]) node["physics"] = False
node["physics"] = False
nodes.append(node) nodes.append(node)
results = {} results = {}
@@ -177,16 +164,11 @@ class TopologyHomeView(PermissionRequiredMixin, View):
topo_data = None topo_data = None
if request.GET: if request.GET:
hide_unconnected = None
if 'hide_unconnected' in request.GET:
if request.GET["hide_unconnected"] == "on" :
hide_unconnected = True
if 'draw_init' in request.GET: if 'draw_init' in request.GET:
if request.GET["draw_init"].lower() == 'true': if bool(distutils.util.strtobool(request.GET["draw_init"])):
topo_data = get_topology_data(self.queryset, hide_unconnected) topo_data = get_topology_data(self.queryset)
else: else:
topo_data = get_topology_data(self.queryset, hide_unconnected) topo_data = get_topology_data(self.queryset)
else: else:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"] preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"] preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
@@ -199,6 +181,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
q.setlist('tag', list(q_tags)) q.setlist('tag', list(q_tags))
q['draw_init'] = settings.PLUGINS_CONFIG["netbox_topology_views"]["draw_default_layout"] q['draw_init'] = settings.PLUGINS_CONFIG["netbox_topology_views"]["draw_default_layout"]
query_string = q.urlencode() query_string = q.urlencode()
print(query_string)
return HttpResponseRedirect(request.path + "?" + query_string) return HttpResponseRedirect(request.path + "?" + query_string)
return render(request, 'netbox_topology_views/index.html' , { return render(request, 'netbox_topology_views/index.html' , {
+79 -53
View File
@@ -1,9 +1,24 @@
{ {
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "1.2.0", "version": "1.0.0-alpha.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
"dev": true,
"requires": {
"@types/hammerjs": "^2.0.36"
}
},
"@types/hammerjs": {
"version": "2.0.38",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.38.tgz",
"integrity": "sha512-wuwDzWW1JWh3BZoRftBlKcctjNzR75QFY4/b4zAz7sH1EesA8HBJzke+bF5dxCATNdHHs3X1P5UWanbbUT6chw==",
"dev": true
},
"abbrev": { "abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -1300,21 +1315,13 @@
"dev": true "dev": true
}, },
"copy-props": { "copy-props": {
"version": "2.0.5", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz",
"integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==",
"dev": true, "dev": true,
"requires": { "requires": {
"each-props": "^1.3.2", "each-props": "^1.3.0",
"is-plain-object": "^5.0.0" "is-plain-object": "^2.0.1"
},
"dependencies": {
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true
}
} }
}, },
"core-util-is": { "core-util-is": {
@@ -2024,9 +2031,9 @@
} }
}, },
"glob-watcher": { "glob-watcher": {
"version": "5.0.5", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz",
"integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==",
"dev": true, "dev": true,
"requires": { "requires": {
"anymatch": "^2.0.0", "anymatch": "^2.0.0",
@@ -2034,16 +2041,7 @@
"chokidar": "^2.0.0", "chokidar": "^2.0.0",
"is-negated-glob": "^1.0.0", "is-negated-glob": "^1.0.0",
"just-debounce": "^1.0.0", "just-debounce": "^1.0.0",
"normalize-path": "^3.0.0",
"object.defaults": "^1.1.0" "object.defaults": "^1.1.0"
},
"dependencies": {
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
}
} }
}, },
"global-modules": { "global-modules": {
@@ -2171,13 +2169,13 @@
} }
}, },
"gulp-sass": { "gulp-sass": {
"version": "4.1.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-4.1.1.tgz", "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-4.1.0.tgz",
"integrity": "sha512-bg7mfgsgho0Ej0WXE9Cd2sq/YxeKxOjagrMmM40zvOYXHtZvi5ED84wIpqCUvJLz66kFNkv+jS/rQXolmgXrUQ==", "integrity": "sha512-xIiwp9nkBLcJDpmYHbEHdoWZv+j+WtYaKD6Zil/67F3nrAaZtWYN5mDwerdo7EvcdBenSAj7Xb2hx2DqURLGdA==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "^2.3.0", "chalk": "^2.3.0",
"lodash": "^4.17.20", "lodash": "^4.17.11",
"node-sass": "^4.8.3", "node-sass": "^4.8.3",
"plugin-error": "^1.0.1", "plugin-error": "^1.0.1",
"replace-ext": "^1.0.0", "replace-ext": "^1.0.0",
@@ -2629,9 +2627,9 @@
"dev": true "dev": true
}, },
"json-schema": { "json-schema": {
"version": "0.4.0", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"dev": true "dev": true
}, },
"json-schema-traverse": { "json-schema-traverse": {
@@ -2653,14 +2651,14 @@
"dev": true "dev": true
}, },
"jsprim": { "jsprim": {
"version": "1.4.2", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"dev": true, "dev": true,
"requires": { "requires": {
"assert-plus": "1.0.0", "assert-plus": "1.0.0",
"extsprintf": "1.3.0", "extsprintf": "1.3.0",
"json-schema": "0.4.0", "json-schema": "0.2.3",
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
@@ -2670,6 +2668,12 @@
"integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=",
"dev": true "dev": true
}, },
"keycharm": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.3.1.tgz",
"integrity": "sha512-zn47Ti4FJT9zdF+YBBLWJsfKF/fYQHkrYlBeB5Ez5e2PjW7SoIxr43yehAne2HruulIoid4NKZZxO0dHBygCtQ==",
"dev": true
},
"kind-of": { "kind-of": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -2930,9 +2934,9 @@
} }
}, },
"minimist": { "minimist": {
"version": "1.2.6", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
}, },
"mixin-deep": { "mixin-deep": {
@@ -2965,6 +2969,12 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"dev": true
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -3389,9 +3399,9 @@
"dev": true "dev": true
}, },
"path-parse": { "path-parse": {
"version": "1.0.7", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true "dev": true
}, },
"path-root": { "path-root": {
@@ -3795,7 +3805,8 @@
"dependencies": { "dependencies": {
"ansi-regex": { "ansi-regex": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true "dev": true
}, },
"camelcase": { "camelcase": {
@@ -4333,6 +4344,12 @@
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=",
"dev": true "dev": true
}, },
"timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"to-absolute-glob": { "to-absolute-glob": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz",
@@ -4579,6 +4596,12 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true "dev": true
}, },
"uuid": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.0.tgz",
"integrity": "sha512-LNUrNsXdI/fUsypJbWM8Jt4DgQdFAZh41p9C7WE9Cn+CULOEkoG2lgQyH68v3wnIy5K3fN4jdSt270K6IFA3MQ==",
"dev": true
},
"v8flags": { "v8flags": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz",
@@ -4679,19 +4702,22 @@
} }
}, },
"vis-data": { "vis-data": {
"version": "7.1.2", "version": "6.6.1",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz", "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-6.6.1.tgz",
"integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw==" "integrity": "sha512-xmujDB2Dzf8T04rGFJ9OP4OA6zRVrz8R9hb0CVKryBrZRCljCga9JjSfgctA8S7wdZu7otDtUIwX4ZOgfV/57w==",
"dev": true
}, },
"vis-network": { "vis-network": {
"version": "9.1.0", "version": "7.10.2",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.0.tgz", "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-7.10.2.tgz",
"integrity": "sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw==" "integrity": "sha512-KDx2agbDnaiE0Bye4AcCRqTn5mxzDKhdUNpKkzSn0AOLBmdhNtPGjxAFluAmvFVyiSK5R6Q5KIWdLjeIMu/PAQ==",
"dev": true
}, },
"vis-util": { "vis-util": {
"version": "5.0.2", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.2.tgz", "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-4.3.4.tgz",
"integrity": "sha512-oPDmPc4o0uQLoKpKai2XD1DjrhYsA7MRz75Wx9KmfX84e9LLgsbno7jVL5tR0K9eNVQkD6jf0Ei8NtbBHDkF1A==" "integrity": "sha512-hJIZNrwf4ML7FYjs+m+zjJfaNvhjk3/1hbMdQZVnwwpOFJS/8dMG8rdbOHXcKoIEM6U5VOh3HNpaDXxGkOZGpw==",
"dev": true
}, },
"which": { "which": {
"version": "1.3.1", "version": "1.3.1",
+12 -13
View File
@@ -1,27 +1,26 @@
{ {
"private": true, "private": true,
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "1.2.0", "version": "1.0.0-alpha.2",
"scripts": { "scripts": {
"resources": "gulp build", "resources": "gulp build",
"resources_dev": "gulp build_dev" "resources_dev": "gulp build_dev"
}, },
"dependencies": { "dependencies": {},
"vis-data": "^7.1.2",
"vis-network": "^9.1.0",
"vis-util": "^5.0.2"
},
"devDependencies": { "devDependencies": {
"@egjs/hammerjs": "^2.0.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0", "gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-sass": "^4.1.0", "gulp-sass": "^4.1.0",
"gulp-uglify": "^3.0.2" "gulp-uglify": "^3.0.2",
"keycharm": "^0.3.0",
"moment": "^2.24.0",
"timsort": "^0.3.0",
"uuid": "7.0.0",
"vis-data": "^6.2.1",
"vis-network": "^7.10.2",
"vis-util": "^4.0.0"
}, },
"peerDependencies": { "peerDependencies": {}
"@egjs/hammerjs": "^2.0.0",
"uuid": "^8.0.0",
"keycharm": "^0.4.0",
"timsort": "^0.3.0"
}
} }
+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='1.2.0', 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',