Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f187e75fff | ||
|
|
c6df846bde | ||
|
|
52c2611e9d | ||
|
|
5c87694b87 | ||
|
|
94f5314649 | ||
|
|
32042ff2cc | ||
|
|
9ce13cd1d2 | ||
|
|
b4e2f2418e | ||
|
|
5563e3c084 | ||
|
|
77a8a2c0d6 | ||
|
|
010179742c | ||
|
|
f2bbaed317 | ||
|
|
8c56f21f16 | ||
|
|
eb627bdedc | ||
|
|
291c033e34 | ||
|
|
1f389b3faa | ||
|
|
3d3ddaa7c4 | ||
|
|
4ea2097429 | ||
|
|
cfaba462d5 | ||
|
|
5e2f1cca57 | ||
|
|
b2e2a4c781 | ||
|
|
318b8495b7 | ||
|
|
5e1d5d2937 | ||
|
|
0b6df3bd28 | ||
|
|
af1666b3ba | ||
|
|
31a8c756e2 |
+2
-1
@@ -128,4 +128,5 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
app.js.map
|
||||||
@@ -29,25 +29,38 @@ Once installed, the plugin needs to be enabled in your `configuration.py`
|
|||||||
PLUGINS = ["netbox_topology_views"]
|
PLUGINS = ["netbox_topology_views"]
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run `python3 manage.py collectstatic --no-input`
|
First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
|
||||||
|
|
||||||
|
|
||||||
|
Then run
|
||||||
|
```bash
|
||||||
|
cd /opt/netbox/netbox
|
||||||
|
pip3 install netbox-topology-views
|
||||||
|
python3 manage.py migrate netbox_topology_views
|
||||||
|
python3 manage.py collectstatic --no-input
|
||||||
|
```
|
||||||
|
|
||||||
### Versions
|
### Versions
|
||||||
|
|
||||||
| netbox version | netbox-topology-views version |
|
| netbox version | netbox-topology-views version |
|
||||||
| ------------- |-------------|
|
| -------------- | ----------------------------- |
|
||||||
| >= 3.2.0 | >= v1.1.0 |
|
| >= 3.3.0 | >= v3.0.0 |
|
||||||
| >= 3.1.8 | >= v1.0.0 |
|
| >= 3.2.0 | >= v1.1.0 |
|
||||||
| >= 2.11.1 | >= v0.5.3 |
|
| >= 3.1.8 | >= v1.0.0 |
|
||||||
| >= 2.10.0 | >= v0.5.0 |
|
| >= 2.11.1 | >= v0.5.3 |
|
||||||
| < 2.10.0 | =< v0.4.10 |
|
| >= 2.10.0 | >= v0.5.0 |
|
||||||
|
| < 2.10.0 | =< v0.4.10 |
|
||||||
|
|
||||||
### Custom field: coordinates
|
### Custom field: coordinates
|
||||||
|
|
||||||
There is also support for custom fields.
|
There is also support for custom fields.
|
||||||
|
|
||||||
If you create a custom field "coordinates" for "dcim > device" with type "text" and name "coordinates" you will see the same layout every time.
|
If you create a custom field "coordinates" for "dcim > device" and "Circuits > circuit" with type "text" and name "coordinates" you will see the same layout every time. It is recommended to set this field to "UI visibility" "Hidden" and let the plugin manage it in the background.
|
||||||
|
|
||||||
The coordinates can then be provided as: "X;Y"
|
The coordinates are stored as: "X;Y".
|
||||||
|
|
||||||
|
Please read the "Configure" chapter to set the `allow_coordinates_saving` option to True.
|
||||||
|
You might also set the `always_save_coordinates` option to True.
|
||||||
|
|
||||||
## Configure
|
## Configure
|
||||||
|
|
||||||
@@ -57,28 +70,25 @@ Example:
|
|||||||
```
|
```
|
||||||
PLUGINS_CONFIG = {
|
PLUGINS_CONFIG = {
|
||||||
'netbox_topology_views': {
|
'netbox_topology_views': {
|
||||||
'device_img': ['router','switch', 'firewall'],
|
'preselected_device_roles': ['Router', 'Firewall'],
|
||||||
'preselected_device_roles': ['Router', 'Firewall']
|
'enable_circuit_terminations': True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| 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. |
|
| static_image_directory | netbox_topology_views/img | (str or pathlib.Path) Specifies the location that images will be loaded from by default. Must be within `STATIC_ROOT` |
|
||||||
| 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 |
|
||||||
| preselected_intermediate_dev_roles | ['Patch-panel'] | The full name of the device roles you want to display in the global view when using end-to-end connections mode. 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 |
|
| always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default |
|
||||||
| ignore_cable_type | ['power outlet', 'power port'] | The cable types that you want to ignore in the views |
|
| ignore_cable_type | [] | 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 |
|
| 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) |
|
||||||
| 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
|
||||||
|
|
||||||
You upload you own custom images to the netbox static dir (`static/netbox_topology_views/img/`).
|
To change image with associated device use the `Images` page - it allows to map a device role with an image found in the netbox static directory (defined by the plugin config `static_image_directory` which defaults to `netbox_topology_views/img`). You can also upload you own custom images to there - these images will automatically be used for a device (if it does not already have a specified image in the settings) if their name is the device role slug.
|
||||||
These images need to be named after de device role slug and have the .png format/extension.
|
|
||||||
If you add your own image you also need to add the slug to the `device_img` setting.
|
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
@@ -88,6 +98,8 @@ Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plu
|
|||||||
|
|
||||||
Run `pip install netbox-topology-views --upgrade` in your venv.
|
Run `pip install netbox-topology-views --upgrade` in your venv.
|
||||||
|
|
||||||
|
Run `python3 manage.py migrate netbox_topology_views`
|
||||||
|
|
||||||
Run `python3 manage.py collectstatic --no-input`
|
Run `python3 manage.py collectstatic --no-input`
|
||||||
|
|
||||||
|
|
||||||
@@ -101,3 +113,12 @@ To view `/plugins/topology-views/` you need the following permissions:
|
|||||||
+ dcim | site | can view site
|
+ dcim | site | can view site
|
||||||
+ extras | tag | can view tag
|
+ extras | tag | can view tag
|
||||||
+ dcim | device role | can view device role
|
+ dcim | device role | can view device role
|
||||||
|
|
||||||
|
## Icons info
|
||||||
|
|
||||||
|
Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power).
|
||||||
|
|
||||||
|
Power icons created by [Flat Icons - Flaticon](https://www.flaticon.com/free-icons/power)
|
||||||
|
|
||||||
|
Provider icons created by [Good Ware - Flaticon](https://www.flaticon.com/free-icons/provider)
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
+ install docker
|
+ install docker
|
||||||
+ install docker compose
|
+ install docker compose
|
||||||
+ install npm
|
+ install npm
|
||||||
|
+ enable yarn
|
||||||
+ clone netbox repo
|
+ clone netbox repo
|
||||||
+ clone netbox_topology_views repo
|
+ clone netbox_topology_views repo
|
||||||
+ create venv: `/usr/bin/python3 -m venv venv`
|
+ create venv: `/usr/bin/python3 -m venv venv`
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
+ build the package: `python3 setup.py develop`
|
+ build the package: `python3 setup.py develop`
|
||||||
+ build the resources `npm run resources`
|
+ build the resources `yarn run bundle`
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
+ copy configuration.py from dev_setup to the netbox/netbox/ folder
|
+ copy configuration.py from dev_setup to the netbox/netbox/ folder
|
||||||
|
|||||||
-78
@@ -1,78 +0,0 @@
|
|||||||
var gulp = require('gulp');
|
|
||||||
var sass = require('gulp-sass');
|
|
||||||
var concat = require('gulp-concat');
|
|
||||||
var uglify = require('gulp-uglify');
|
|
||||||
var cleanCss = require('gulp-clean-css');
|
|
||||||
|
|
||||||
|
|
||||||
var jsVendorFiles = [
|
|
||||||
'node_modules/vis-network/standalone/umd/vis-network.min.js',
|
|
||||||
];
|
|
||||||
|
|
||||||
var cssVendorFiles = [
|
|
||||||
'node_modules/vis-network/dist/dist/vis-network.min.css',
|
|
||||||
];
|
|
||||||
var sassFiles = './netbox_topology_views/static_dev/css/*.scss',
|
|
||||||
cssDest = './netbox_topology_views/static/netbox_topology_views/css/';
|
|
||||||
|
|
||||||
var jsLocalResourceFiles = './netbox_topology_views/static_dev/js/*.js',
|
|
||||||
jsDest = './netbox_topology_views/static/netbox_topology_views/js/';
|
|
||||||
|
|
||||||
|
|
||||||
function styles_local() {
|
|
||||||
return gulp.src(sassFiles)
|
|
||||||
.pipe(sass().on('error', sass.logError))
|
|
||||||
.pipe(concat('app.css'))
|
|
||||||
.pipe(cleanCss())
|
|
||||||
.pipe(gulp.dest(cssDest));
|
|
||||||
|
|
||||||
}
|
|
||||||
exports.styles_local = styles_local
|
|
||||||
|
|
||||||
function styles_vendor() {
|
|
||||||
return gulp.src(cssVendorFiles)
|
|
||||||
.pipe(concat('vendor.css'))
|
|
||||||
.pipe(cleanCss())
|
|
||||||
.pipe(gulp.dest(cssDest))
|
|
||||||
}
|
|
||||||
exports.styles_vendor = styles_vendor
|
|
||||||
|
|
||||||
function js_local() {
|
|
||||||
return gulp.src(jsLocalResourceFiles)
|
|
||||||
.pipe(concat('app.js'))
|
|
||||||
.pipe(uglify())
|
|
||||||
.pipe(gulp.dest(jsDest))
|
|
||||||
}
|
|
||||||
exports.js_local = js_local
|
|
||||||
|
|
||||||
function js_vendor() {
|
|
||||||
return gulp.src(jsVendorFiles)
|
|
||||||
.pipe(concat('vendor.js'))
|
|
||||||
.pipe(uglify())
|
|
||||||
.pipe(gulp.dest(jsDest))
|
|
||||||
|
|
||||||
}
|
|
||||||
exports.js_vendor = js_vendor
|
|
||||||
|
|
||||||
function js_local_dev() {
|
|
||||||
return gulp.src(jsLocalResourceFiles)
|
|
||||||
.pipe(concat('app.js'))
|
|
||||||
.pipe(gulp.dest(jsDest))
|
|
||||||
}
|
|
||||||
exports.js_local_dev = js_local_dev
|
|
||||||
|
|
||||||
function js_vendor_dev() {
|
|
||||||
return gulp.src(jsVendorFiles)
|
|
||||||
.pipe(concat('vendor.js'))
|
|
||||||
.pipe(gulp.dest(jsDest))
|
|
||||||
|
|
||||||
}
|
|
||||||
exports.js_vendor_dev = js_vendor_dev
|
|
||||||
|
|
||||||
exports.css = gulp.series(styles_local, styles_vendor);
|
|
||||||
|
|
||||||
exports.js = gulp.series(js_local, js_vendor);
|
|
||||||
exports.js_dev = gulp.series(js_local_dev, js_vendor_dev);
|
|
||||||
|
|
||||||
exports.build = gulp.series(exports.css, exports.js);
|
|
||||||
exports.build_dev = gulp.series(exports.css, exports.js_dev);
|
|
||||||
@@ -1,24 +1,35 @@
|
|||||||
from extras.plugins import PluginConfig
|
from extras.plugins import PluginConfig
|
||||||
|
|
||||||
|
|
||||||
class TopologyViewsConfig(PluginConfig):
|
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 = '2.0.2'
|
version = "3.1.0"
|
||||||
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"
|
||||||
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": [
|
||||||
'preselected_intermediate_dev_roles' : [],
|
"Firewall",
|
||||||
'end2end_connections': False,
|
"Router",
|
||||||
'ignore_cable_type': ['power outlet','power port'],
|
"Distribution Switch",
|
||||||
'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'],
|
"Core Switch",
|
||||||
'allow_coordinates_saving': False,
|
"Internal Switch",
|
||||||
'preselected_tags' : [],
|
"Access Switch",
|
||||||
'enable_circuit_terminations': False,
|
"Server",
|
||||||
'draw_default_layout': False
|
"Storage",
|
||||||
|
"Backup",
|
||||||
|
"Wireless AP",
|
||||||
|
],
|
||||||
|
"ignore_cable_type": [],
|
||||||
|
"static_image_directory": "netbox_topology_views/img",
|
||||||
|
"allow_coordinates_saving": False,
|
||||||
|
"always_save_coordinates": False,
|
||||||
|
"preselected_tags": [],
|
||||||
|
"draw_default_layout": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
config = TopologyViewsConfig
|
config = TopologyViewsConfig
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""REST API"""
|
"""REST API"""
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
|
from dcim.models import Device, DeviceRole
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
from dcim.models import DeviceRole, Device
|
from netbox_topology_views.models import RoleImage
|
||||||
from extras.models import Tag
|
|
||||||
|
|
||||||
|
|
||||||
class TopologyDummySerializer(ModelSerializer):
|
class TopologyDummySerializer(ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = ('id', 'name')
|
fields = ("id", "name")
|
||||||
|
|
||||||
|
|
||||||
|
class RoleImageSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RoleImage
|
||||||
|
fields = ("role", "image")
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRoleSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = DeviceRole
|
||||||
|
fields = ("name", "slug", "color", "vm_role", "description")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from rest_framework import routers
|
from netbox.api.routers import NetBoxRouter
|
||||||
from . import views
|
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
from netbox_topology_views.api import views
|
||||||
|
|
||||||
router.register('save-coords', views.SaveCoordsViewSet, basename='save_coords')
|
router = NetBoxRouter()
|
||||||
|
|
||||||
urlpatterns = router.urls
|
router.register("save-coords", views.SaveCoordsViewSet)
|
||||||
|
router.register("images", views.SaveRoleImageViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
|
|||||||
@@ -1,60 +1,128 @@
|
|||||||
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet, GenericViewSet
|
from typing import Dict
|
||||||
|
|
||||||
|
from circuits.models import Circuit
|
||||||
|
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.http import JsonResponse
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||||
from rest_framework.routers import APIRootView
|
|
||||||
|
|
||||||
from .serializers import TopologyDummySerializer
|
from netbox_topology_views.api.serializers import (
|
||||||
from django.conf import settings
|
RoleImageSerializer,
|
||||||
|
TopologyDummySerializer,
|
||||||
from dcim.models import DeviceRole, Device, Cable
|
)
|
||||||
from circuits.models import Circuit
|
from netbox_topology_views.models import RoleImage
|
||||||
from extras.models import Tag
|
from netbox_topology_views.utils import get_image_from_url
|
||||||
|
|
||||||
|
|
||||||
class TopologyViewsRootView(APIRootView):
|
|
||||||
def get_view_name(self):
|
|
||||||
return 'TopologyViews'
|
|
||||||
|
|
||||||
|
|
||||||
class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.none()
|
||||||
serializer_class = TopologyDummySerializer
|
serializer_class = TopologyDummySerializer
|
||||||
|
|
||||||
@action(detail=False, methods=['patch'])
|
@action(detail=False, methods=["patch"])
|
||||||
def save_coords(self, request):
|
def save_coords(self, request):
|
||||||
results = {}
|
if not settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||||
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
|
"allow_coordinates_saving"
|
||||||
device_id = None
|
]:
|
||||||
x_coord = None
|
return Response({"status": "not allowed to save coords"}, status=500)
|
||||||
y_coord = None
|
|
||||||
if "node_id" in request.data:
|
|
||||||
if request.data["node_id"]:
|
|
||||||
device_id = request.data["node_id"]
|
|
||||||
if "x" in request.data:
|
|
||||||
if request.data["x"]:
|
|
||||||
x_coord = request.data["x"]
|
|
||||||
if "y" in request.data:
|
|
||||||
if request.data["y"]:
|
|
||||||
y_coord = request.data["y"]
|
|
||||||
|
|
||||||
actual_device= Device.objects.get(id=device_id)
|
device_id: str = request.data.get("node_id", None)
|
||||||
|
x_coord = request.data.get("x", None)
|
||||||
|
y_coord = request.data.get("y", None)
|
||||||
|
|
||||||
if "coordinates" in actual_device.custom_field_data:
|
actual_device = None
|
||||||
actual_device.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
|
if device_id.startswith("c"):
|
||||||
actual_device.save()
|
device_id = device_id.lstrip("c")
|
||||||
results["status"] = "saved coords"
|
actual_device = Circuit.objects.get(id=device_id)
|
||||||
else:
|
elif device_id.startswith("p"):
|
||||||
try:
|
device_id = device_id.lstrip("p")
|
||||||
actual_device.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
|
actual_device = PowerPanel.objects.get(id=device_id)
|
||||||
actual_device.save()
|
elif device_id.startswith("f"):
|
||||||
results["status"] = "saved coords"
|
device_id = device_id.lstrip("f")
|
||||||
except :
|
actual_device = PowerFeed.objects.get(id=device_id)
|
||||||
results["status"] = "coords custom field not created"
|
elif device_id.isnumeric():
|
||||||
return Response(status=500)
|
actual_device = Device.objects.get(id=device_id)
|
||||||
|
|
||||||
return Response(results)
|
if not actual_device:
|
||||||
else:
|
return Response({"status": "invalid node_id in body"}, status=400)
|
||||||
results["status"] = "not allowed to save coords"
|
|
||||||
return Response(results, status=500)
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
actual_device.custom_field_data["coordinates"] = "%s;%s" % (
|
||||||
|
x_coord,
|
||||||
|
y_coord,
|
||||||
|
)
|
||||||
|
actual_device.save()
|
||||||
|
except:
|
||||||
|
return Response(
|
||||||
|
{"status": "coords custom field could not be saved"}, status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({"status": "saved coords"})
|
||||||
|
|
||||||
|
|
||||||
|
class SaveRoleImageViewSet(PermissionRequiredMixin, ViewSet):
|
||||||
|
queryset = DeviceRole.objects.none()
|
||||||
|
serializer_class = RoleImageSerializer
|
||||||
|
permission_required = (
|
||||||
|
"dcim.add_device_role",
|
||||||
|
"dcim.change_device_role",
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request):
|
||||||
|
if not isinstance(request.data, dict):
|
||||||
|
return JsonResponse(
|
||||||
|
{"status": "Missing or malformed request body"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
device_roles = {k: v for k, v in request.data.items() if k.isnumeric()}
|
||||||
|
content_type_ids = {
|
||||||
|
k[2:]: v
|
||||||
|
for k, v in request.data.items()
|
||||||
|
if k.startswith("ct") and k[2:].isnumeric()
|
||||||
|
}
|
||||||
|
|
||||||
|
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
|
||||||
|
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
|
||||||
|
content_type_ids.keys()
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(roles) != len(device_roles):
|
||||||
|
difference = set(device_roles) - set(roles.keys())
|
||||||
|
return JsonResponse(
|
||||||
|
{"status": f"Got unknown device role ids: {difference}"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(content_types) != len(content_type_ids):
|
||||||
|
difference = set(content_type_ids) - set(content_types.keys())
|
||||||
|
return JsonResponse(
|
||||||
|
{"status": f"Got unknown content type ids: {difference}"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if device_roles:
|
||||||
|
device_role_ct = ContentType.objects.get_for_model(DeviceRole)
|
||||||
|
|
||||||
|
for id, url in device_roles.items():
|
||||||
|
RoleImage.objects.update_or_create(
|
||||||
|
{
|
||||||
|
"content_type_id": device_role_ct.pk,
|
||||||
|
"object_id": id,
|
||||||
|
"image": str(get_image_from_url(url)),
|
||||||
|
},
|
||||||
|
object_id=id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for content_type_id, url in content_type_ids.items():
|
||||||
|
RoleImage.objects.update_or_create(
|
||||||
|
{
|
||||||
|
"content_type_id": content_type_id,
|
||||||
|
"image": str(get_image_from_url(url)),
|
||||||
|
},
|
||||||
|
content_type_id=content_type_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({"status": "Ok"})
|
||||||
|
|||||||
@@ -1,59 +1,55 @@
|
|||||||
from cProfile import label
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from dcim.models import Device, DeviceRole, Region, Site, Location
|
|
||||||
from dcim.choices import DeviceStatusChoices
|
from dcim.choices import DeviceStatusChoices
|
||||||
|
from dcim.models import Device, DeviceRole, Location, Rack, Region, Site
|
||||||
|
from django.db.models import Q
|
||||||
from netbox.filtersets import NetBoxModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet
|
||||||
|
|
||||||
from tenancy.models import TenantGroup, Tenant
|
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
|
|
||||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method="search",
|
||||||
label='Search',
|
label="Search",
|
||||||
)
|
)
|
||||||
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(),
|
||||||
label='Role (ID)',
|
label="Role (ID)",
|
||||||
)
|
)
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='site__region',
|
field_name="site__region",
|
||||||
lookup_expr='in',
|
lookup_expr="in",
|
||||||
label='Region (ID)',
|
label="Region (ID)",
|
||||||
)
|
)
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
label='Site (ID)',
|
label="Site (ID)",
|
||||||
)
|
)
|
||||||
location_id = TreeNodeMultipleChoiceFilter(
|
location_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
field_name='location',
|
field_name="location",
|
||||||
lookup_expr='in',
|
lookup_expr="in",
|
||||||
label='Location (ID)',
|
label="Location (ID)",
|
||||||
|
)
|
||||||
|
rack_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
field_name="rack_id",
|
||||||
|
label="Rack (ID)",
|
||||||
)
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=DeviceStatusChoices,
|
choices=DeviceStatusChoices,
|
||||||
null_value=None
|
null_value=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
qs_filter = (
|
qs_filter = Q(name__icontains=value)
|
||||||
Q(name__icontains=value)
|
|
||||||
)
|
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.models import Device, Site, Region, DeviceRole, Location
|
from dcim.models import Device, Site, Region, DeviceRole, Location, Rack
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from dcim.choices import DeviceStatusChoices
|
from dcim.choices import DeviceStatusChoices
|
||||||
@@ -13,74 +13,108 @@ from tenancy.models import TenantGroup, Tenant
|
|||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, MultipleChoiceField)
|
from utilities.forms import (
|
||||||
|
TagFilterField,
|
||||||
|
DynamicModelMultipleChoiceField,
|
||||||
|
MultipleChoiceField,
|
||||||
|
)
|
||||||
|
|
||||||
allow_coordinates_saving = settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
|
allow_coordinates_saving = bool(
|
||||||
end2end = settings.PLUGINS_CONFIG["netbox_topology_views"]["end2end_connections"]
|
settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||||
model = Device
|
model = Device
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'hide_unconnected', 'save_coords', 'end2end_connections')),
|
(
|
||||||
(None, ('tenant_group_id', 'tenant_id')),
|
None,
|
||||||
(None, ('region_id', 'site_id', 'location_id')),
|
(
|
||||||
(None, ('device_role_id', 'intermediate_dev_role_id')),
|
"q",
|
||||||
(None, ('tag', 'status')),
|
"hide_unconnected",
|
||||||
|
"save_coords",
|
||||||
|
"show_circuit",
|
||||||
|
"show_power",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
(
|
||||||
|
"tenant_group_id",
|
||||||
|
"tenant_id",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(None, ("region_id", "site_id", "location_id", "rack_id")),
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
(
|
||||||
|
"device_role_id",
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(None, ("tag",)),
|
||||||
)
|
)
|
||||||
|
|
||||||
region_id = DynamicModelMultipleChoiceField(
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(), required=False, label=_("Region")
|
||||||
required=False,
|
|
||||||
label=_('Region')
|
|
||||||
)
|
)
|
||||||
device_role_id = DynamicModelMultipleChoiceField(
|
device_role_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=DeviceRole.objects.all(),
|
queryset=DeviceRole.objects.all(), required=False, label=_("Device Role")
|
||||||
required=False,
|
|
||||||
label=_('Device Role')
|
|
||||||
)
|
)
|
||||||
end2end_connections = forms.BooleanField(
|
id = DynamicModelMultipleChoiceField(
|
||||||
label=_("Display end-to-end connections"),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
initial=end2end
|
label=_("Device"),
|
||||||
)
|
query_params={
|
||||||
intermediate_dev_role_id = DynamicModelMultipleChoiceField(
|
"location_id": "$location_id",
|
||||||
queryset=DeviceRole.objects.all(),
|
"region_id": "$region_id",
|
||||||
required=False,
|
"site_id": "$site_id",
|
||||||
label=_('Intermediate Devices Role'),
|
"role_id": "$device_role_id",
|
||||||
help_text='Intermediate devices to display when using end-to-end connections mode, even if they do not match the query'
|
},
|
||||||
)
|
)
|
||||||
site_id = DynamicModelMultipleChoiceField(
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Site.objects.all(),
|
queryset=Site.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'region_id': '$region_id',
|
"region_id": "$region_id",
|
||||||
},
|
},
|
||||||
label=_('Site')
|
label=_("Site"),
|
||||||
)
|
)
|
||||||
location_id = DynamicModelMultipleChoiceField(
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Location.objects.all(),
|
queryset=Location.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'region_id': '$region_id',
|
"region_id": "$region_id",
|
||||||
'site_id': '$site_id',
|
"site_id": "$site_id",
|
||||||
},
|
},
|
||||||
label=_('Location')
|
label=_("Location"),
|
||||||
|
)
|
||||||
|
rack_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Rack.objects.all(),
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
"region_id": "$region_id",
|
||||||
|
"site_id": "$site_id",
|
||||||
|
"location_id": "$location_id",
|
||||||
|
},
|
||||||
|
label=_("Rack"),
|
||||||
)
|
)
|
||||||
hide_unconnected = forms.BooleanField(
|
hide_unconnected = forms.BooleanField(
|
||||||
label=_("Hide Unconnected"),
|
label=_("Hide Unconnected"), required=False, initial=False
|
||||||
required=False,
|
)
|
||||||
initial=False
|
show_circuit = forms.BooleanField(
|
||||||
|
label=_("Show Circuit Terminations"), required=False, initial=False
|
||||||
|
)
|
||||||
|
show_power = forms.BooleanField(
|
||||||
|
label=_("Show Power Feeds"), required=False, initial=False
|
||||||
)
|
)
|
||||||
save_coords = forms.BooleanField(
|
save_coords = forms.BooleanField(
|
||||||
label=_("Save Coordinates"),
|
label=_("Save Coordinates"),
|
||||||
required=False,
|
required=False,
|
||||||
disabled=(not allow_coordinates_saving),
|
disabled=(not allow_coordinates_saving),
|
||||||
initial=False
|
|
||||||
)
|
)
|
||||||
status = MultipleChoiceField(
|
status = MultipleChoiceField(
|
||||||
choices=DeviceStatusChoices,
|
choices=DeviceStatusChoices, required=False, label=_("Device Status")
|
||||||
required=False,
|
|
||||||
label=_("Device Status")
|
|
||||||
)
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 4.0.8 on 2022-12-06 13:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RoleImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||||
|
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('image', models.CharField(max_length=255)),
|
||||||
|
('object_id', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='roleimage',
|
||||||
|
index=models.Index(fields=['content_type', 'object_id'], name='netbox_topo_content_9d87d4_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dcim.models import DeviceRole
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import models
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from netbox.models.features import (
|
||||||
|
ChangeLoggingMixin,
|
||||||
|
ExportTemplatesMixin,
|
||||||
|
WebhooksMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
from netbox_topology_views.utils import (
|
||||||
|
CONF_IMAGE_DIR,
|
||||||
|
IMAGE_DIR,
|
||||||
|
Role,
|
||||||
|
find_image_url,
|
||||||
|
get_model_role,
|
||||||
|
image_static_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
||||||
|
class Meta:
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=["content_type", "object_id"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
objects: "models.Manager[RoleImage]"
|
||||||
|
|
||||||
|
image = models.CharField("Path within the netbox static directory", max_length=255)
|
||||||
|
|
||||||
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
|
||||||
|
__role: Optional[Role] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def role(self) -> Role:
|
||||||
|
if self.__role:
|
||||||
|
return self.__role
|
||||||
|
|
||||||
|
model_class = self.content_type.model_class()
|
||||||
|
|
||||||
|
if not model_class:
|
||||||
|
raise ValueError(f"Invalid content type: {self.content_type}")
|
||||||
|
|
||||||
|
if model_class == DeviceRole:
|
||||||
|
device_role: DeviceRole = DeviceRole.objects.get(pk=self.object_id)
|
||||||
|
self.__role = Role(slug=device_role.slug, name=device_role.name)
|
||||||
|
return self.__role
|
||||||
|
|
||||||
|
self.__role = get_model_role(model_class)
|
||||||
|
return self.__role
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.role} - {self.image}"
|
||||||
|
|
||||||
|
def get_image(self) -> Path:
|
||||||
|
"""Get Icon
|
||||||
|
|
||||||
|
returns the model's image's absolute path in the filesystem
|
||||||
|
raises ValueError if the file cannot be found
|
||||||
|
"""
|
||||||
|
path = Path(settings.STATIC_ROOT) / self.image
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise ValueError(f"{self.role} path '{path}' does not exists")
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_default_image(self, dir: Path = CONF_IMAGE_DIR):
|
||||||
|
"""Get default image
|
||||||
|
|
||||||
|
will attempt to find image in given directory with any file extension,
|
||||||
|
otherwise will try to find a `role-unknown` image
|
||||||
|
|
||||||
|
fallback is `STATIC_ROOT/netbox_topology_views/img/role-unknown.png`
|
||||||
|
"""
|
||||||
|
if url := find_image_url(self.role.slug, dir):
|
||||||
|
return url
|
||||||
|
|
||||||
|
# fallback to default role unknown image
|
||||||
|
return image_static_url(IMAGE_DIR / "role-unknown.png")
|
||||||
|
|
||||||
|
def get_image_url(self, dir: Path = CONF_IMAGE_DIR) -> str:
|
||||||
|
try:
|
||||||
|
self.get_image()
|
||||||
|
except ValueError:
|
||||||
|
return self.get_default_image(dir)
|
||||||
|
return static(f"/{self.image}")
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
from extras.plugins import PluginMenuItem
|
||||||
from utilities.choices import ButtonColorChoices
|
|
||||||
|
|
||||||
menu_items = (
|
menu_items = (
|
||||||
|
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
|
||||||
PluginMenuItem(
|
PluginMenuItem(
|
||||||
link='plugins:netbox_topology_views:home',
|
link="plugins:netbox_topology_views:images", link_text="Topology Images"
|
||||||
link_text='Topology'
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}
|
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
(()=>{var l=(e,t,o)=>new Promise((n,r)=>{var c=s=>{try{i(o.next(s))}catch(a){r(a)}},m=s=>{try{i(o.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,m);i((o=o.apply(e,t)).next())});var d=e=>{if(!document.cookie)return;let t=null,o=document.cookie.split(";");for(let n=0;n<o.length;n++){let r=o[n].trim();if(r.substring(0,e.length+1)===e+"="){t=decodeURIComponent(r.substring(e.length+1));break}}return t};var u={success:e=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()},error:e=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()}};var g={},p=d("csrftoken");document.querySelector("form#images").addEventListener("submit",e=>l(void 0,null,function*(){e.preventDefault();try{let t=yield fetch("/api/plugins/netbox_topology_views/images/",{method:"POST",body:JSON.stringify(g),headers:{"X-CSRFToken":p,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(e=>{e.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:o,image:n}}=t.currentTarget;g[o]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${o}`);r&&(r.innerHTML=`<img src="${n}" />`)})});})();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
|
||||||
|
"mappings": "mNAAO,GAAM,GAAY,AAAC,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,GAAI,GAAc,KACZ,EAAU,SAAS,OAAO,MAAM,KAEtC,OAAS,GAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,GAAM,GAAS,EAAQ,GAAG,OAE1B,GAAI,EAAO,UAAU,EAAG,EAAK,OAAS,KAAO,EAAO,IAAK,CACrD,EAAc,mBAAmB,EAAO,UAAU,EAAK,OAAS,IAChE,OAIR,MAAO,ICfJ,GAAM,GAAQ,CACjB,QAAS,AAAC,GAAY,CAClB,GAAM,GAAK,SAAS,cAAc,kCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,QAEV,MAAO,AAAC,GAAY,CAChB,GAAM,GAAK,SAAS,cAAc,gCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,SCZd,GAAM,GAAU,GACV,EAAY,EAAU,aAE5B,SAAS,cAAc,eAAe,iBAAiB,SAAU,AAAO,GAAM,0BAC1E,EAAE,iBACF,GAAI,CACA,GAAM,GAAM,KAAM,OAAM,6CAA8C,CAClE,OAAQ,OACR,KAAM,KAAK,UAAU,GACrB,QAAS,CACL,cAAe,EACf,eAAgB,sBAIxB,GAAI,CAAC,EAAI,GAAI,KAAM,IAAI,OAAM,KAAM,GAAI,QACvC,EAAM,QAAQ,wBACT,EAAP,CACE,QAAQ,IAAI,GACZ,EAAM,MAAM,EAAI,aAIxB,SAAS,iBAAiB,kCAAkC,QAAQ,AAAC,GAAO,CACxE,EAAG,iBAAiB,QAAS,AAAC,GAAM,CA3BxC,MA4BQ,GAAI,CAAE,GAAE,wBAAyB,cAAc,OAC/C,GAAM,CACF,QAAS,CAAE,OAAM,UACjB,EAAE,cAEN,EAAQ,GAAQ,EAEhB,GAAM,GAAS,KAAE,cACZ,QAAQ,eADE,cAET,cAAc,sBAAsB,KAC1C,AAAI,GAAQ,GAAO,UAAY,aAAa",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
|
|||||||
|
const esbuild = require('esbuild')
|
||||||
|
const { sassPlugin } = require('esbuild-sass-plugin')
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: 'external',
|
||||||
|
sourcesContent: false,
|
||||||
|
logLevel: 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ARGS = process.argv.slice(2)
|
||||||
|
const noCache = ARGS.includes('--no-cache')
|
||||||
|
|
||||||
|
async function bundleScripts() {
|
||||||
|
const entryPoints = {
|
||||||
|
app: 'js/home.js',
|
||||||
|
images: 'js/images.js'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await esbuild.build({
|
||||||
|
...options,
|
||||||
|
outdir: '../static/netbox_topology_views/js/',
|
||||||
|
entryPoints,
|
||||||
|
target: 'es2016'
|
||||||
|
})
|
||||||
|
if (result.errors.length !== 0) return
|
||||||
|
|
||||||
|
for (const [targetName, sourceName] of Object.entries(entryPoints)) {
|
||||||
|
const source = sourceName.split('/').pop() // take last element
|
||||||
|
console.log(
|
||||||
|
`✅ Bundled source file '${source}' to '${targetName}.js'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundleStyles() {
|
||||||
|
try {
|
||||||
|
const entryPoints = {
|
||||||
|
vendor: 'css/_external.scss',
|
||||||
|
app: 'css/app.scss'
|
||||||
|
}
|
||||||
|
const pluginOptions = { outputStyle: 'compressed' }
|
||||||
|
// Allow cache disabling.
|
||||||
|
if (noCache) {
|
||||||
|
pluginOptions.cache = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await esbuild.build({
|
||||||
|
...options,
|
||||||
|
outdir: '../static/netbox_topology_views/css/',
|
||||||
|
// Disable sourcemaps for CSS/SCSS files, see #7068
|
||||||
|
sourcemap: false,
|
||||||
|
entryPoints,
|
||||||
|
plugins: [sassPlugin(pluginOptions)],
|
||||||
|
loader: {
|
||||||
|
'.eot': 'file',
|
||||||
|
'.woff': 'file',
|
||||||
|
'.woff2': 'file',
|
||||||
|
'.svg': 'file',
|
||||||
|
'.ttf': 'file'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (result.errors.length === 0) {
|
||||||
|
for (const [targetName, sourceName] of Object.entries(
|
||||||
|
entryPoints
|
||||||
|
)) {
|
||||||
|
const source = sourceName.split('/')[1]
|
||||||
|
console.log(
|
||||||
|
`✅ Bundled source file '${source}' to '${targetName}.css'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundleAll() {
|
||||||
|
if (ARGS.includes('--styles')) {
|
||||||
|
// Only run style jobs.
|
||||||
|
return await bundleStyles()
|
||||||
|
}
|
||||||
|
if (ARGS.includes('--scripts')) {
|
||||||
|
// Only run script jobs.
|
||||||
|
return await bundleScripts()
|
||||||
|
}
|
||||||
|
await bundleStyles()
|
||||||
|
await bundleScripts()
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleAll()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import '../node_modules/vis-network/dist/dist/vis-network.min.css';
|
||||||
@@ -1,7 +1,27 @@
|
|||||||
#visgraph {
|
#visgraph {
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-netbox-color-mode=dark] #visgraph {
|
html[data-netbox-color-mode=dark] #visgraph {
|
||||||
background-color: #212529;
|
background-color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.image-dropdown img {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-dropdown-content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
padding-inline: 0.5rem;
|
||||||
|
width: 50vw;
|
||||||
|
max-width: 32rem;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const getCookie = (name) => {
|
||||||
|
if (!document.cookie) return
|
||||||
|
|
||||||
|
let cookieValue = null
|
||||||
|
const cookies = document.cookie.split(';')
|
||||||
|
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const 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
|
||||||
|
}
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
var graph = null;
|
import { DataSet } from 'vis-data/esnext'
|
||||||
var container = null;
|
import { Network } from 'vis-network/esnext'
|
||||||
var downloadButton = null;
|
import { getCookie } from './csrftoken.js'
|
||||||
var MIME_TYPE = "image/png";
|
|
||||||
var canvas = null;
|
const options = {
|
||||||
var csrftoken = null;
|
|
||||||
var nodes = new vis.DataSet();
|
|
||||||
var edges = new vis.DataSet();
|
|
||||||
var options = {
|
|
||||||
interaction: {
|
interaction: {
|
||||||
hover: true,
|
hover: true,
|
||||||
hoverConnectedEdges: true,
|
hoverConnectedEdges: true,
|
||||||
multiselect: false
|
multiselect: true
|
||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
shape: 'image',
|
shape: 'image',
|
||||||
brokenImage: '../../static/netbox_topology_views/img/role-unknown.png',
|
brokenImage: brokenImage ?? '',
|
||||||
size: 35,
|
size: 35,
|
||||||
font: {
|
font: {
|
||||||
multi: 'md',
|
multi: 'md',
|
||||||
face: 'helvetica',
|
face: 'helvetica',
|
||||||
},
|
color:
|
||||||
|
document.documentElement.dataset.netboxColorMode === 'dark'
|
||||||
|
? '#fff'
|
||||||
|
: '#000'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
edges: {
|
edges: {
|
||||||
length: 100,
|
length: 100,
|
||||||
width: 2,
|
width: 2,
|
||||||
font: {
|
font: {
|
||||||
face: 'helvetica',
|
face: 'helvetica'
|
||||||
},
|
},
|
||||||
shadow: {
|
shadow: {
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -34,128 +34,114 @@ var options = {
|
|||||||
physics: {
|
physics: {
|
||||||
solver: 'forceAtlas2Based'
|
solver: 'forceAtlas2Based'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
var selected_regions = [];
|
|
||||||
var selected_sites = [];
|
|
||||||
var coord_save_checkbox = null;
|
|
||||||
var htmlElement = null;
|
|
||||||
|
|
||||||
function getCookie(name) {
|
// Load CSRF token
|
||||||
var cookieValue = null;
|
const csrftoken = getCookie('csrftoken')
|
||||||
if (document.cookie && document.cookie !== '') {
|
|
||||||
var cookies = document.cookie.split(';');
|
// Render vis graph
|
||||||
for (var i = 0; i < cookies.length; i++) {
|
let graph = null // vis graph instance
|
||||||
var cookie = cookies[i].trim();
|
|
||||||
// Does this cookie string begin with the name we want?
|
const container = document.querySelector('#visgraph')
|
||||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
;(function handleLoadData() {
|
||||||
break;
|
if (!topologyData) return
|
||||||
}
|
|
||||||
}
|
function htmlTitle(text) {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.innerHTML = text
|
||||||
|
return container
|
||||||
}
|
}
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const nodes = new DataSet(
|
||||||
|
topologyData.nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
title: htmlTitle(node.title)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
function htmlTitle(html) {
|
const edges = new DataSet(
|
||||||
container = document.createElement("div");
|
topologyData.edges.map((node) => ({
|
||||||
container.innerHTML = html;
|
...node,
|
||||||
return container;
|
title: htmlTitle(node.title)
|
||||||
}
|
}))
|
||||||
|
)
|
||||||
|
graph = new Network(container, { nodes, edges }, options)
|
||||||
|
graph.fit()
|
||||||
|
|
||||||
function addEdge(item) {
|
graph.on('dragEnd', (params) => {
|
||||||
item.title = htmlTitle( item.title );
|
if (!coordSaveCheckbox.checked) return
|
||||||
edges.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNode(item) {
|
Promise.allSettled(
|
||||||
item.title = htmlTitle( item.title );
|
Object.entries(graph.getPositions(params.nodes)).map(
|
||||||
nodes.add(item);
|
async ([nodeId, nodePosition]) => {
|
||||||
}
|
const res = await fetch(
|
||||||
|
'/api/plugins/netbox_topology_views/save-coords/save_coords/',
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
node_id: nodeId,
|
||||||
|
x: nodePosition.x,
|
||||||
|
y: nodePosition.y
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
function iniPlotboxIndex() {
|
console.log(nodeId, res.status, res.statusText)
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
}
|
||||||
csrftoken = getCookie('csrftoken');
|
)
|
||||||
container = document.getElementById('visgraph');
|
)
|
||||||
htmlElement = document.getElementsByTagName("html")[0];
|
})
|
||||||
downloadButton = document.getElementById('btnDownloadImage');
|
|
||||||
handleLoadData();
|
graph.on('doubleClick', (params) => {
|
||||||
btnFullView = document.getElementById('btnFullView');
|
params.nodes.forEach((node) => {
|
||||||
coord_save_checkbox = document.getElementById('id_save_coords');
|
window.open(nodes.get(node).href, '_blank')
|
||||||
}, false);
|
})
|
||||||
}
|
})
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Download Graph
|
||||||
|
const MIME_TYPE = 'image/png'
|
||||||
|
|
||||||
|
const downloadButton = document.querySelector('#btnDownloadImage')
|
||||||
|
downloadButton.addEventListener('click', (e) => {
|
||||||
|
performGraphDownload()
|
||||||
|
})
|
||||||
|
|
||||||
function performGraphDownload() {
|
function performGraphDownload() {
|
||||||
var tempDownloadLink = document.createElement('a');
|
const canvas = container.querySelector('canvas')
|
||||||
var generatedImageUrl = canvas.toDataURL(MIME_TYPE);
|
const tempDownloadLink = document.createElement('a')
|
||||||
|
const generatedImageUrl = canvas.toDataURL(MIME_TYPE)
|
||||||
|
|
||||||
tempDownloadLink.href = generatedImageUrl;
|
tempDownloadLink.href = generatedImageUrl
|
||||||
tempDownloadLink.download = "topology";
|
tempDownloadLink.download = 'topology'
|
||||||
document.body.appendChild(tempDownloadLink);
|
document.body.appendChild(tempDownloadLink)
|
||||||
tempDownloadLink.click();
|
tempDownloadLink.click()
|
||||||
document.body.removeChild(tempDownloadLink);
|
document.body.removeChild(tempDownloadLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLoadData() {
|
// Theme switching
|
||||||
if (topology_data !== null) {
|
const observer = new MutationObserver((mutations) =>
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
if (htmlElement.dataset.netboxColorMode == "dark") {
|
if (
|
||||||
options.nodes.font.color = "#fff";
|
!graph ||
|
||||||
}
|
mutation.type !== 'attributes' ||
|
||||||
|
mutation.attributeName !== 'data-netbox-color-mode' ||
|
||||||
|
!(mutation.target instanceof HTMLElement)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
const { netboxColorMode } = mutation.target.dataset
|
||||||
|
options.nodes.font.color = netboxColorMode === 'dark' ? '#fff' : '#000'
|
||||||
|
graph.setOptions(options)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
graph = null;
|
observer.observe(document.documentElement, {
|
||||||
nodes = new vis.DataSet();
|
attributes: true,
|
||||||
edges = new vis.DataSet();
|
attributeFilter: ['data-netbox-color-mode']
|
||||||
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) { performGraphDownload(); return false; };
|
|
||||||
|
|
||||||
graph.on("dragEnd", function (params) {
|
|
||||||
dragged = this.getPositions(params.nodes);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
graph.on("doubleClick", function (params) {
|
|
||||||
console.log(params.nodes);
|
|
||||||
selected_devices = params.nodes;
|
|
||||||
for (selected_device in selected_devices) {
|
|
||||||
url = "/dcim/devices/" + selected_devices[selected_device] + "/";
|
|
||||||
window.open(url, "_blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { getCookie } from './csrftoken.js'
|
||||||
|
import { toast } from './toast.js'
|
||||||
|
|
||||||
|
const mapping = {}
|
||||||
|
const csrftoken = getCookie('csrftoken')
|
||||||
|
|
||||||
|
document.querySelector('form#images').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/plugins/netbox_topology_views/images/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(mapping),
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrftoken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(await res.text())
|
||||||
|
toast.success('Saved settings')
|
||||||
|
} catch (err) {
|
||||||
|
console.dir(err)
|
||||||
|
toast.error(err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelectorAll('form#images .dropdown-menu img').forEach((el) => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
if (!(e.currentTarget instanceof HTMLElement)) return
|
||||||
|
const {
|
||||||
|
dataset: { role, image }
|
||||||
|
} = e.currentTarget
|
||||||
|
|
||||||
|
mapping[role] = image
|
||||||
|
|
||||||
|
const button = e.currentTarget
|
||||||
|
.closest('.dropdown')
|
||||||
|
?.querySelector(`#dropdownMenuButton${role}`)
|
||||||
|
if (button) button.innerHTML = `<img src="${image}" />`
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const toast = {
|
||||||
|
success: (message) => {
|
||||||
|
const el = document.querySelector('#topology-plugin-success-toast')
|
||||||
|
if (!el) return console.error('Could not find toast component!')
|
||||||
|
const content = el.querySelector('span')
|
||||||
|
content.textContent = message
|
||||||
|
const toast = new window.Toast(el)
|
||||||
|
toast.show()
|
||||||
|
},
|
||||||
|
error: (message) => {
|
||||||
|
const el = document.querySelector('#topology-plugin-error-toast')
|
||||||
|
if (!el) return console.error('Could not find toast component!')
|
||||||
|
const content = el.querySelector('span')
|
||||||
|
content.textContent = message
|
||||||
|
const toast = new window.Toast(el)
|
||||||
|
toast.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
+454
@@ -0,0 +1,454 @@
|
|||||||
|
{
|
||||||
|
"name": "netbox_topology_views",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@esbuild/linux-loong64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/hammerjs": {
|
||||||
|
"version": "2.0.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
|
||||||
|
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"anymatch": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"picomatch": "^2.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary-extensions": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"braces": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fill-range": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chokidar": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"fsevents": "~2.3.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"component-emitter": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"version": "0.12.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
|
||||||
|
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"esbuild-android-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-android-arm64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-darwin-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-darwin-arm64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-freebsd-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-freebsd-arm64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-32": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-arm": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-arm64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-mips64le": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-ppc64le": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-riscv64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-linux-s390x": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-netbsd-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-openbsd-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-sass-plugin": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"esbuild": "^0.14.5",
|
||||||
|
"picomatch": "^2.3.0",
|
||||||
|
"resolve": "^1.20.0",
|
||||||
|
"sass": "^1.45.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@esbuild/linux-loong64": "0.14.54",
|
||||||
|
"esbuild-android-64": "0.14.54",
|
||||||
|
"esbuild-android-arm64": "0.14.54",
|
||||||
|
"esbuild-darwin-64": "0.14.54",
|
||||||
|
"esbuild-darwin-arm64": "0.14.54",
|
||||||
|
"esbuild-freebsd-64": "0.14.54",
|
||||||
|
"esbuild-freebsd-arm64": "0.14.54",
|
||||||
|
"esbuild-linux-32": "0.14.54",
|
||||||
|
"esbuild-linux-64": "0.14.54",
|
||||||
|
"esbuild-linux-arm": "0.14.54",
|
||||||
|
"esbuild-linux-arm64": "0.14.54",
|
||||||
|
"esbuild-linux-mips64le": "0.14.54",
|
||||||
|
"esbuild-linux-ppc64le": "0.14.54",
|
||||||
|
"esbuild-linux-riscv64": "0.14.54",
|
||||||
|
"esbuild-linux-s390x": "0.14.54",
|
||||||
|
"esbuild-netbsd-64": "0.14.54",
|
||||||
|
"esbuild-openbsd-64": "0.14.54",
|
||||||
|
"esbuild-sunos-64": "0.14.54",
|
||||||
|
"esbuild-windows-32": "0.14.54",
|
||||||
|
"esbuild-windows-64": "0.14.54",
|
||||||
|
"esbuild-windows-arm64": "0.14.54"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"esbuild-sunos-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-windows-32": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-windows-64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild-windows-arm64": {
|
||||||
|
"version": "0.14.54",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz",
|
||||||
|
"integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"fill-range": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"function-bind": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"function-bind": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"immutable": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-core-module": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"has": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"is-glob": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-number": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"keycharm": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"picomatch": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"resolve": {
|
||||||
|
"version": "1.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||||
|
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-core-module": "^2.9.0",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"version": "1.56.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz",
|
||||||
|
"integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
|
"immutable": "^4.0.0",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source-map-js": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"timsort": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"to-regex-range": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"vis-data": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w=="
|
||||||
|
},
|
||||||
|
"vis-network": {
|
||||||
|
"version": "9.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz",
|
||||||
|
"integrity": "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw=="
|
||||||
|
},
|
||||||
|
"vis-util": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "netbox_topology_views",
|
||||||
|
"version": "3.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"bundle": "node bundle.js",
|
||||||
|
"bundle:styles": "node bundle.js --styles",
|
||||||
|
"bundle:scripts": "node bundle.js --scripts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vis-data": "^7.1.4",
|
||||||
|
"vis-network": "^9.1.2",
|
||||||
|
"vis-util": "^5.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.12.24",
|
||||||
|
"esbuild-sass-plugin": "^1.5.2",
|
||||||
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
|
"component-emitter": "^1.3.0",
|
||||||
|
"keycharm": "^0.4.0",
|
||||||
|
"timsort": "^0.3.0",
|
||||||
|
"uuid": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@egjs/hammerjs@^2.0.0":
|
||||||
|
version "2.0.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
|
||||||
|
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
|
||||||
|
dependencies:
|
||||||
|
"@types/hammerjs" "^2.0.36"
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64@0.14.53":
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6"
|
||||||
|
integrity sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==
|
||||||
|
|
||||||
|
"@types/hammerjs@^2.0.36":
|
||||||
|
version "2.0.41"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa"
|
||||||
|
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==
|
||||||
|
|
||||||
|
anymatch@~3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
|
||||||
|
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
|
||||||
|
dependencies:
|
||||||
|
normalize-path "^3.0.0"
|
||||||
|
picomatch "^2.0.4"
|
||||||
|
|
||||||
|
binary-extensions@^2.0.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||||
|
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||||
|
|
||||||
|
braces@~3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||||
|
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||||
|
dependencies:
|
||||||
|
fill-range "^7.0.1"
|
||||||
|
|
||||||
|
"chokidar@>=3.0.0 <4.0.0":
|
||||||
|
version "3.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||||
|
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
|
||||||
|
dependencies:
|
||||||
|
anymatch "~3.1.2"
|
||||||
|
braces "~3.0.2"
|
||||||
|
glob-parent "~5.1.2"
|
||||||
|
is-binary-path "~2.1.0"
|
||||||
|
is-glob "~4.0.1"
|
||||||
|
normalize-path "~3.0.0"
|
||||||
|
readdirp "~3.6.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
component-emitter@^1.3.0:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||||
|
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
|
||||||
|
|
||||||
|
esbuild-android-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz#259bc3ef1399a3cad8f4f67c40ee20779c4de675"
|
||||||
|
integrity sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==
|
||||||
|
|
||||||
|
esbuild-android-arm64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e"
|
||||||
|
integrity sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==
|
||||||
|
|
||||||
|
esbuild-darwin-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a"
|
||||||
|
integrity sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==
|
||||||
|
|
||||||
|
esbuild-darwin-arm64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9"
|
||||||
|
integrity sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==
|
||||||
|
|
||||||
|
esbuild-freebsd-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0"
|
||||||
|
integrity sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==
|
||||||
|
|
||||||
|
esbuild-freebsd-arm64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9"
|
||||||
|
integrity sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==
|
||||||
|
|
||||||
|
esbuild-linux-32@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5"
|
||||||
|
integrity sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==
|
||||||
|
|
||||||
|
esbuild-linux-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4"
|
||||||
|
integrity sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==
|
||||||
|
|
||||||
|
esbuild-linux-arm64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe"
|
||||||
|
integrity sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==
|
||||||
|
|
||||||
|
esbuild-linux-arm@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf"
|
||||||
|
integrity sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==
|
||||||
|
|
||||||
|
esbuild-linux-mips64le@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f"
|
||||||
|
integrity sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==
|
||||||
|
|
||||||
|
esbuild-linux-ppc64le@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139"
|
||||||
|
integrity sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==
|
||||||
|
|
||||||
|
esbuild-linux-riscv64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00"
|
||||||
|
integrity sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==
|
||||||
|
|
||||||
|
esbuild-linux-s390x@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129"
|
||||||
|
integrity sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==
|
||||||
|
|
||||||
|
esbuild-netbsd-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz#e86d0efd0116658be335492ed12e66b26b4baf52"
|
||||||
|
integrity sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==
|
||||||
|
|
||||||
|
esbuild-openbsd-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b"
|
||||||
|
integrity sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==
|
||||||
|
|
||||||
|
esbuild-sass-plugin@^1.5.2:
|
||||||
|
version "1.8.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz#76babbd6d4c87ffd14259fd364d446f857b3f513"
|
||||||
|
integrity sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==
|
||||||
|
dependencies:
|
||||||
|
esbuild "^0.14.5"
|
||||||
|
picomatch "^2.3.0"
|
||||||
|
resolve "^1.20.0"
|
||||||
|
sass "^1.45.0"
|
||||||
|
|
||||||
|
esbuild-sunos-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8"
|
||||||
|
integrity sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==
|
||||||
|
|
||||||
|
esbuild-windows-32@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709"
|
||||||
|
integrity sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==
|
||||||
|
|
||||||
|
esbuild-windows-64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af"
|
||||||
|
integrity sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==
|
||||||
|
|
||||||
|
esbuild-windows-arm64@0.14.53:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8"
|
||||||
|
integrity sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==
|
||||||
|
|
||||||
|
esbuild@^0.12.24:
|
||||||
|
version "0.12.29"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.29.tgz#be602db7c4dc78944a9dbde0d1ea19d36c1f882d"
|
||||||
|
integrity sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==
|
||||||
|
|
||||||
|
esbuild@^0.14.5:
|
||||||
|
version "0.14.53"
|
||||||
|
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.53.tgz#20b1007f686e8584f2a01a1bec5a37aac9498ce4"
|
||||||
|
integrity sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==
|
||||||
|
optionalDependencies:
|
||||||
|
"@esbuild/linux-loong64" "0.14.53"
|
||||||
|
esbuild-android-64 "0.14.53"
|
||||||
|
esbuild-android-arm64 "0.14.53"
|
||||||
|
esbuild-darwin-64 "0.14.53"
|
||||||
|
esbuild-darwin-arm64 "0.14.53"
|
||||||
|
esbuild-freebsd-64 "0.14.53"
|
||||||
|
esbuild-freebsd-arm64 "0.14.53"
|
||||||
|
esbuild-linux-32 "0.14.53"
|
||||||
|
esbuild-linux-64 "0.14.53"
|
||||||
|
esbuild-linux-arm "0.14.53"
|
||||||
|
esbuild-linux-arm64 "0.14.53"
|
||||||
|
esbuild-linux-mips64le "0.14.53"
|
||||||
|
esbuild-linux-ppc64le "0.14.53"
|
||||||
|
esbuild-linux-riscv64 "0.14.53"
|
||||||
|
esbuild-linux-s390x "0.14.53"
|
||||||
|
esbuild-netbsd-64 "0.14.53"
|
||||||
|
esbuild-openbsd-64 "0.14.53"
|
||||||
|
esbuild-sunos-64 "0.14.53"
|
||||||
|
esbuild-windows-32 "0.14.53"
|
||||||
|
esbuild-windows-64 "0.14.53"
|
||||||
|
esbuild-windows-arm64 "0.14.53"
|
||||||
|
|
||||||
|
fill-range@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||||
|
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||||
|
dependencies:
|
||||||
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
|
fsevents@~2.3.2:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||||
|
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||||
|
|
||||||
|
function-bind@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||||
|
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||||
|
|
||||||
|
glob-parent@~5.1.2:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||||
|
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||||
|
dependencies:
|
||||||
|
is-glob "^4.0.1"
|
||||||
|
|
||||||
|
has@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
|
||||||
|
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
|
||||||
|
dependencies:
|
||||||
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
immutable@^4.0.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
|
||||||
|
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
|
||||||
|
|
||||||
|
is-binary-path@~2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||||
|
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
|
||||||
|
dependencies:
|
||||||
|
binary-extensions "^2.0.0"
|
||||||
|
|
||||||
|
is-core-module@^2.9.0:
|
||||||
|
version "2.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
|
||||||
|
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
|
||||||
|
dependencies:
|
||||||
|
has "^1.0.3"
|
||||||
|
|
||||||
|
is-extglob@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||||
|
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
|
||||||
|
|
||||||
|
is-glob@^4.0.1, is-glob@~4.0.1:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||||
|
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||||
|
dependencies:
|
||||||
|
is-extglob "^2.1.1"
|
||||||
|
|
||||||
|
is-number@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
|
||||||
|
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||||
|
|
||||||
|
keycharm@^0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/keycharm/-/keycharm-0.4.0.tgz#8d684ea9cc01379a07fbddee33ff32d97f5ae2a7"
|
||||||
|
integrity sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==
|
||||||
|
|
||||||
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
|
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||||
|
|
||||||
|
path-parse@^1.0.7:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||||
|
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||||
|
|
||||||
|
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.0:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
|
readdirp@~3.6.0:
|
||||||
|
version "3.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
|
||||||
|
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
|
||||||
|
dependencies:
|
||||||
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
|
resolve@^1.20.0:
|
||||||
|
version "1.22.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||||
|
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
|
||||||
|
dependencies:
|
||||||
|
is-core-module "^2.9.0"
|
||||||
|
path-parse "^1.0.7"
|
||||||
|
supports-preserve-symlinks-flag "^1.0.0"
|
||||||
|
|
||||||
|
sass@^1.45.0:
|
||||||
|
version "1.54.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.1.tgz#4f72ef57ce2a6c3251f4e2c75eee9a0c19e09eb5"
|
||||||
|
integrity sha512-GHJJr31Me32RjjUBagyzx8tzjKBUcDwo5239XANIRBq0adDu5iIG0aFO0i/TBb/4I9oyxkEv44nq/kL1DxdDhA==
|
||||||
|
dependencies:
|
||||||
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
|
immutable "^4.0.0"
|
||||||
|
source-map-js ">=0.6.2 <2.0.0"
|
||||||
|
|
||||||
|
"source-map-js@>=0.6.2 <2.0.0":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
|
supports-preserve-symlinks-flag@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||||
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
|
timsort@^0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
|
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
|
||||||
|
|
||||||
|
to-regex-range@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||||
|
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
|
||||||
|
dependencies:
|
||||||
|
is-number "^7.0.0"
|
||||||
|
|
||||||
|
uuid@^8.0.0:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
|
vis-data@^7.1.4:
|
||||||
|
version "7.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.4.tgz#90e5e796a79e1901de14c0808fb32a1a0735c1dc"
|
||||||
|
integrity sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==
|
||||||
|
|
||||||
|
vis-network@^9.1.2:
|
||||||
|
version "9.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-9.1.2.tgz#ddaca300a9764bbf1646a0aa03eb28fe55823ca8"
|
||||||
|
integrity sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==
|
||||||
|
|
||||||
|
vis-util@^5.0.3:
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/vis-util/-/vis-util-5.0.3.tgz#6a9d67c6ed92f2bfa67e50c20166bb5f59ac07bf"
|
||||||
|
integrity sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Topology Views Images{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content-wrapper %}
|
||||||
|
<form id="images" class="container py-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-3 mb-2">
|
||||||
|
{% for role in roles %}
|
||||||
|
<div class="d-flex flex-column align-items-center">
|
||||||
|
<div class="image-dropdown dropdown">
|
||||||
|
<button class="btn btn-secondary" type="button" id="dropdownMenuButton{{ role.id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{% if role.image %}
|
||||||
|
<img src="{{ role.image }}" />
|
||||||
|
{% else %}
|
||||||
|
Image
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu mt-2" aria-labelledby="dropdownMenuButton{{ role.id }}">
|
||||||
|
<div class="image-dropdown-content">
|
||||||
|
{% for image in images %}
|
||||||
|
<img src="{{ image.url }}" title="{{ image.title }}" width="64" data-role="{{ role.id }}" data-image="{{ image.url }}"/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{{ role.name }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
{% include 'netbox_topology_views/toasts.html' %}
|
||||||
|
{% endblock content-wrapper %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'netbox_topology_views/js/images.js' %}" defer></script>
|
||||||
|
{% endblock javascript %}
|
||||||
@@ -50,12 +50,8 @@
|
|||||||
|
|
||||||
<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"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var topology_data = {{ topology_data | safe }};
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if filter_form %}
|
{% if filter_form %}
|
||||||
@@ -69,7 +65,9 @@
|
|||||||
{% endblock content-wrapper %}
|
{% endblock content-wrapper %}
|
||||||
|
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
<script type="text/javascript">
|
||||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
const brokenImage = '{{ broken_image }}';
|
||||||
<script type="application/javascript">iniPlotboxIndex()</script>
|
const topologyData = {{ topology_data | safe }};
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
|
||||||
{% endblock javascript %}
|
{% endblock javascript %}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<div id="django-messages" class="toast-container">
|
||||||
|
<div
|
||||||
|
id="topology-plugin-error-toast"
|
||||||
|
class="toast align-items-center border-0 bg-danger hide"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
data-bs-delay="2000"
|
||||||
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
😢
|
||||||
|
<span>Error</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close me-2 m-auto"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="topology-plugin-success-toast"
|
||||||
|
class="toast align-items-center border-0 bg-success hide"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
data-bs-delay="2000"
|
||||||
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
👍
|
||||||
|
<span>Success</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close me-2 m-auto"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
|
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
|
||||||
# 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("", RedirectView.as_view(url="topology/", permanent=True)),
|
||||||
|
path("topology/", views.TopologyHomeView.as_view(), name="home"),
|
||||||
|
path("images/", views.TopologyImagesView.as_view(), name="images"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.utils.text import camel_case_to_spaces, re_camel_case
|
||||||
|
|
||||||
|
IMAGE_DIR = Path(settings.STATIC_ROOT) / "netbox_topology_views/img"
|
||||||
|
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / settings.PLUGINS_CONFIG[
|
||||||
|
"netbox_topology_views"
|
||||||
|
]["static_image_directory"].removeprefix("/")
|
||||||
|
|
||||||
|
|
||||||
|
def image_static_url(path: Path) -> str:
|
||||||
|
return settings.BASE_PATH + static(
|
||||||
|
f"/{path.relative_to(Path(settings.STATIC_ROOT))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_from_url(url: str) -> str:
|
||||||
|
return url.removeprefix(settings.BASE_PATH + settings.STATIC_URL)
|
||||||
|
|
||||||
|
|
||||||
|
IMAGE_FILETYPES = (
|
||||||
|
"apng",
|
||||||
|
"avif",
|
||||||
|
"bmp",
|
||||||
|
"cur",
|
||||||
|
"gif",
|
||||||
|
"ico",
|
||||||
|
"jfif",
|
||||||
|
"jpeg",
|
||||||
|
"jpg",
|
||||||
|
"pjp",
|
||||||
|
"pjpeg",
|
||||||
|
"png",
|
||||||
|
"svg",
|
||||||
|
"webp",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_image_in_dir(glob: str, dir: Path):
|
||||||
|
return next(
|
||||||
|
(f for f in dir.glob(f"{glob}.*") if f.suffix.lstrip(".") in IMAGE_FILETYPES),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=50)
|
||||||
|
def find_image_url(glob: str, dir: Path = CONF_IMAGE_DIR):
|
||||||
|
"""
|
||||||
|
will attempt to find a file that matches glob in given directory with any file extension,
|
||||||
|
otherwise will try to find a `role-unknown` image
|
||||||
|
|
||||||
|
returns static file url
|
||||||
|
"""
|
||||||
|
if file := find_image_in_dir(glob, dir):
|
||||||
|
return image_static_url(file)
|
||||||
|
|
||||||
|
if glob != "role-unknown" and (file := find_image_in_dir("role-unknown", dir)):
|
||||||
|
return image_static_url(file)
|
||||||
|
|
||||||
|
if dir != IMAGE_DIR and (file := find_image_in_dir("role-unknown", IMAGE_DIR)):
|
||||||
|
return image_static_url(file)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Role:
|
||||||
|
slug: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_slug(model: Type[Model]):
|
||||||
|
return camel_case_to_spaces(model.__name__).replace(" ", "-")
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_role(model: Type[Model]) -> Role:
|
||||||
|
return Role(
|
||||||
|
slug=get_model_slug(model),
|
||||||
|
name=re_camel_case.sub(r" \1", model.__name__),
|
||||||
|
)
|
||||||
+595
-230
@@ -1,313 +1,678 @@
|
|||||||
from django.shortcuts import get_object_or_404, render
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.views.generic import View
|
|
||||||
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 json
|
||||||
|
from functools import reduce
|
||||||
|
from typing import DefaultDict, Dict, Optional, Union
|
||||||
|
|
||||||
from dcim.models import Device, Cable, DeviceRole, PathEndpoint
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from circuits.models import CircuitTermination, ProviderNetwork
|
from dcim.models import (
|
||||||
|
Cable,
|
||||||
|
CableTermination,
|
||||||
|
Device,
|
||||||
|
DeviceRole,
|
||||||
|
FrontPort,
|
||||||
|
Interface,
|
||||||
|
PowerFeed,
|
||||||
|
PowerPanel,
|
||||||
|
RearPort,
|
||||||
|
)
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q, QuerySet
|
||||||
|
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.views.generic import View
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
|
from wireless.models import WirelessLink
|
||||||
|
|
||||||
|
from netbox_topology_views.filters import DeviceFilterSet
|
||||||
|
from netbox_topology_views.forms import DeviceFilterForm
|
||||||
|
from netbox_topology_views.models import RoleImage
|
||||||
|
from netbox_topology_views.utils import (
|
||||||
|
CONF_IMAGE_DIR,
|
||||||
|
find_image_url,
|
||||||
|
get_model_role,
|
||||||
|
get_model_slug,
|
||||||
|
image_static_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
supported_termination_types = [
|
||||||
|
"interface",
|
||||||
|
"front port",
|
||||||
|
"rear port",
|
||||||
|
"power outlet",
|
||||||
|
"power port",
|
||||||
|
"console port",
|
||||||
|
"console server port",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_node(device):
|
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
|
||||||
dev_name = device.name
|
is_device = isinstance(entity, Device)
|
||||||
if dev_name is None:
|
query = (
|
||||||
dev_name = "device name unknown"
|
{"object_id": entity.device_role_id}
|
||||||
|
if is_device
|
||||||
|
else {"content_type_id": ContentType.objects.get_for_model(entity).pk}
|
||||||
|
)
|
||||||
|
|
||||||
node_content = ""
|
try:
|
||||||
|
return RoleImage.objects.get(**query).get_image_url()
|
||||||
|
except RoleImage.DoesNotExist:
|
||||||
|
return find_image_url(
|
||||||
|
entity.device_role.slug if is_device else get_model_slug(entity.__class__)
|
||||||
|
)
|
||||||
|
|
||||||
if device.device_type is not None:
|
|
||||||
node_content += "<tr><th>Type: </th><td>" + device.device_type.model + "</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>"
|
|
||||||
if device.site is not None:
|
|
||||||
node_content += "<tr><th>Site: </th><td>" + device.site.name + "</td></tr>"
|
|
||||||
if device.location is not None:
|
|
||||||
node_content += "<tr><th>Location: </th><td>" + device.location.name + "</td></tr>"
|
|
||||||
if device.rack is not None:
|
|
||||||
node_content += "<tr><th>Rack: </th><td>" + device.rack.name + "</td></tr>"
|
|
||||||
if device.position is not None:
|
|
||||||
if device.face is not None:
|
|
||||||
node_content += "<tr><th>Position: </th><td> {} ({}) </td></tr>".format(device.position, device.face)
|
|
||||||
else:
|
|
||||||
node_content += "<tr><th>Position: </th><td>" + device.position + "</td></tr>"
|
|
||||||
|
|
||||||
dev_title = "<table> %s </table>" % (node_content)
|
|
||||||
|
|
||||||
|
def create_node(
|
||||||
|
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool
|
||||||
|
):
|
||||||
node = {}
|
node = {}
|
||||||
node["id"] = device.id
|
node_content = ""
|
||||||
|
if isinstance(device, Circuit):
|
||||||
|
dev_name = f"Circuit {device.cid}"
|
||||||
|
node["id"] = f"c{device.pk}"
|
||||||
|
|
||||||
|
if device.provider is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Provider: </th><td>{device.provider.name}</td></tr>"
|
||||||
|
)
|
||||||
|
if device.type is not None:
|
||||||
|
node_content += f"<tr><th>Type: </th><td>{device.type.name}</td></tr>"
|
||||||
|
elif isinstance(device, PowerPanel):
|
||||||
|
dev_name = f"Power Panel {device.pk}"
|
||||||
|
node["id"] = f"p{device.pk}"
|
||||||
|
|
||||||
|
if device.site is not None:
|
||||||
|
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
|
||||||
|
if device.location is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Location: </th><td>{device.location.name}</td></tr>"
|
||||||
|
)
|
||||||
|
elif isinstance(device, PowerFeed):
|
||||||
|
dev_name = f"Power Feed {device.pk}"
|
||||||
|
node["id"] = f"f{device.pk}"
|
||||||
|
|
||||||
|
if device.power_panel is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Power Panel: </th><td>{device.power_panel.name}</td></tr>"
|
||||||
|
)
|
||||||
|
if device.type is not None:
|
||||||
|
node_content += f"<tr><th>Type: </th><td>{device.type}</td></tr>"
|
||||||
|
if device.supply is not None:
|
||||||
|
node_content += f"<tr><th>Supply: </th><td>{device.supply}</td></tr>"
|
||||||
|
if device.phase is not None:
|
||||||
|
node_content += f"<tr><th>Phase: </th><td>{device.phase}</td></tr>"
|
||||||
|
if device.amperage is not None:
|
||||||
|
node_content += f"<tr><th>Amperage: </th><td>{device.amperage}</td></tr>"
|
||||||
|
if device.voltage is not None:
|
||||||
|
node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>"
|
||||||
|
else:
|
||||||
|
dev_name = device.name
|
||||||
|
if dev_name is None:
|
||||||
|
dev_name = "device name unknown"
|
||||||
|
|
||||||
|
if device.device_type is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Type: </th><td>{device.device_type.model}</td></tr>"
|
||||||
|
)
|
||||||
|
if device.device_role.name is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Role: </th><td>{device.device_role.name}</td></tr>"
|
||||||
|
)
|
||||||
|
if device.serial != "":
|
||||||
|
node_content += f"<tr><th>Serial: </th><td>{device.serial}</td></tr>"
|
||||||
|
if device.primary_ip is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>IP Address: </th><td>{device.primary_ip.address}</td></tr>"
|
||||||
|
)
|
||||||
|
if device.site is not None:
|
||||||
|
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
|
||||||
|
if device.location is not None:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Location: </th><td>{device.location.name}</td></tr>"
|
||||||
|
)
|
||||||
|
if device.rack is not None:
|
||||||
|
node_content += f"<tr><th>Rack: </th><td>{device.rack.name}</td></tr>"
|
||||||
|
if device.position is not None:
|
||||||
|
if device.face is not None:
|
||||||
|
node_content += f"<tr><th>Position: </th><td>{device.position} ({device.face})</td></tr>"
|
||||||
|
else:
|
||||||
|
node_content += (
|
||||||
|
f"<tr><th>Position: </th><td>{device.position}</td></tr>"
|
||||||
|
)
|
||||||
|
|
||||||
|
node["id"] = device.pk
|
||||||
|
|
||||||
|
if device.device_role.color != "":
|
||||||
|
node["color.border"] = "#" + device.device_role.color
|
||||||
|
|
||||||
|
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
|
||||||
|
|
||||||
|
node["title"] = dev_title
|
||||||
node["name"] = dev_name
|
node["name"] = dev_name
|
||||||
node["label"] = dev_name
|
node["label"] = dev_name
|
||||||
node["title"] = dev_title
|
|
||||||
node["shape"] = "image"
|
node["shape"] = "image"
|
||||||
if device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
|
node["href"] = device.get_absolute_url()
|
||||||
node["image"] = "../../static/netbox_topology_views/img/" + device.device_role.slug + ".png"
|
node["image"] = get_image_for_entity(device)
|
||||||
else:
|
|
||||||
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
|
|
||||||
|
|
||||||
if device.device_role.color != "":
|
|
||||||
node["color.border"] = "#" + device.device_role.color
|
|
||||||
|
|
||||||
|
node["physics"] = True
|
||||||
if "coordinates" in device.custom_field_data:
|
if "coordinates" in device.custom_field_data:
|
||||||
if device.custom_field_data["coordinates"] is not None:
|
if device.custom_field_data["coordinates"] is not None:
|
||||||
if ";" in device.custom_field_data["coordinates"]:
|
if ";" in device.custom_field_data["coordinates"]:
|
||||||
cords = device.custom_field_data["coordinates"].split(";")
|
cords = 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
|
||||||
|
elif save_coords:
|
||||||
|
node["physics"] = False
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def create_edge(edge_id, cable, termination_a, termination_b, path = None, circuit = None):
|
|
||||||
cable_a_dev_name = "device A name unknown" if termination_a.device.name is None else termination_a.device.name
|
def create_edge(
|
||||||
cable_a_name = "device A name unknown" if termination_a.name is None else termination_a.name
|
edge_id: int,
|
||||||
cable_b_dev_name = "device A name unknown" if termination_b.device.name is None else termination_b.device.name
|
termination_a: Dict,
|
||||||
cable_b_name = "cable B name unknown" if termination_b.name is None else termination_b.name
|
termination_b: Dict,
|
||||||
|
circuit: Optional[Dict] = None,
|
||||||
|
cable: Optional[Cable] = None,
|
||||||
|
wireless: Optional[Dict] = None,
|
||||||
|
power: Optional[bool] = None,
|
||||||
|
):
|
||||||
|
cable_a_name = (
|
||||||
|
"device A name unknown"
|
||||||
|
if termination_a["termination_name"] is None
|
||||||
|
else termination_a["termination_name"]
|
||||||
|
)
|
||||||
|
cable_a_dev_name = (
|
||||||
|
"device A name unknown"
|
||||||
|
if termination_a["termination_device_name"] is None
|
||||||
|
else termination_a["termination_device_name"]
|
||||||
|
)
|
||||||
|
cable_b_name = (
|
||||||
|
"device A name unknown"
|
||||||
|
if termination_b["termination_name"] is None
|
||||||
|
else termination_b["termination_name"]
|
||||||
|
)
|
||||||
|
cable_b_dev_name = (
|
||||||
|
"cable B name unknown"
|
||||||
|
if termination_b["termination_device_name"] is None
|
||||||
|
else termination_b["termination_device_name"]
|
||||||
|
)
|
||||||
|
|
||||||
edge = {}
|
edge = {}
|
||||||
edge["id"] = edge_id
|
edge["id"] = edge_id
|
||||||
edge["from"] = termination_a.device.id
|
edge["from"] = termination_a["device_id"]
|
||||||
edge["to"] = termination_b.device.id
|
edge["to"] = termination_b["device_id"]
|
||||||
|
title = "Cable"
|
||||||
|
|
||||||
if circuit is not None:
|
if circuit is not None:
|
||||||
edge["dashes"] = True
|
edge["dashes"] = True
|
||||||
edge["title"] = "Circuit provider: " + circuit.provider.name + "<br>"
|
title = f"Circuit provider: {circuit['provider_name']}<br>Termination"
|
||||||
edge["title"] += "Termination between <br>"
|
|
||||||
edge["title"] += cable_b_dev_name + " [" + cable_b_name + "]<br>"
|
elif wireless is not None:
|
||||||
edge["title"] += cable_a_dev_name + " [" + cable_a_name + "]"
|
edge["dashes"] = [2, 10, 2, 10]
|
||||||
else:
|
title = "Wireless Connection"
|
||||||
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
|
|
||||||
|
elif power is not None:
|
||||||
if path is not None:
|
edge["dashes"] = [5, 5, 3, 3]
|
||||||
edge["title"] += "" if len(path) <= 0 else "<br>Through " + "/".join(path)
|
title = "Power Connection"
|
||||||
|
|
||||||
|
edge[
|
||||||
|
"title"
|
||||||
|
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}]"
|
||||||
|
|
||||||
if cable is not None and cable.color != "":
|
if cable is not None and cable.color != "":
|
||||||
edge["color"] = "#" + cable.color
|
edge["color"] = "#" + cable.color
|
||||||
|
|
||||||
return edge
|
return edge
|
||||||
|
|
||||||
def get_topology_data(queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections):
|
|
||||||
nodes_devices = {}
|
def create_circuit_termination(termination):
|
||||||
edges = []
|
if isinstance(termination, CircuitTermination):
|
||||||
edge_ids = 0
|
return {
|
||||||
cable_ids = []
|
"termination_name": termination.circuit.provider.name,
|
||||||
circuit_ids = []
|
"termination_device_name": termination.circuit.cid,
|
||||||
|
"device_id": "c{}".format(termination.circuit.pk),
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isinstance(termination, Interface)
|
||||||
|
or isinstance(termination, FrontPort)
|
||||||
|
or isinstance(termination, RearPort)
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"termination_name": termination.name,
|
||||||
|
"termination_device_name": termination.device.name,
|
||||||
|
"device_id": termination.device.pk,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_topology_data(
|
||||||
|
queryset: QuerySet,
|
||||||
|
hide_unconnected: bool,
|
||||||
|
save_coords: bool,
|
||||||
|
show_circuit: bool,
|
||||||
|
show_power: bool,
|
||||||
|
):
|
||||||
if not queryset:
|
if not queryset:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
|
nodes_devices = {}
|
||||||
enable_circuit_terminations = settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]
|
edges = []
|
||||||
|
nodes = []
|
||||||
|
edge_ids = 0
|
||||||
|
nodes_circuits: Dict[int, Circuit] = {}
|
||||||
|
nodes_powerpanel: Dict[int, PowerPanel] = {}
|
||||||
|
nodes_powerfeed: Dict[int, PowerFeed] = {}
|
||||||
|
nodes_provider_networks = {}
|
||||||
|
cable_ids = DefaultDict(dict)
|
||||||
|
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||||
|
"ignore_cable_type"
|
||||||
|
]
|
||||||
|
|
||||||
device_ids = [d.id for d in queryset]
|
device_ids = [d.pk for d in queryset]
|
||||||
|
site_ids = [d.site_id for d in queryset]
|
||||||
|
|
||||||
links = Cable.objects.filter( Q(_termination_a_device_id__in=device_ids) | Q(_termination_b_device_id__in=device_ids) ) \
|
if show_circuit:
|
||||||
.select_related("termination_a_type", "termination_b_type") \
|
circuit_terminations = CircuitTermination.objects.filter(
|
||||||
.prefetch_related("termination_a", "termination_b")
|
Q(site_id__in=site_ids) | Q(provider_network__isnull=False)
|
||||||
|
).prefetch_related("provider_network", "circuit")
|
||||||
|
for circuit_termination in circuit_terminations:
|
||||||
|
circuit_termination: CircuitTermination
|
||||||
|
if (
|
||||||
|
not hide_unconnected
|
||||||
|
and circuit_termination.circuit_id not in nodes_circuits
|
||||||
|
):
|
||||||
|
nodes_circuits[
|
||||||
|
circuit_termination.circuit.pk
|
||||||
|
] = circuit_termination.circuit
|
||||||
|
|
||||||
|
termination_a = {}
|
||||||
|
termination_b = {}
|
||||||
|
circuit_model = {}
|
||||||
|
if circuit_termination.cable is not None:
|
||||||
|
termination_a = create_circuit_termination(
|
||||||
|
circuit_termination.cable.a_terminations[0]
|
||||||
|
)
|
||||||
|
termination_b = create_circuit_termination(
|
||||||
|
circuit_termination.cable.b_terminations[0]
|
||||||
|
)
|
||||||
|
elif circuit_termination.provider_network is not None:
|
||||||
|
if (
|
||||||
|
circuit_termination.provider_network_id
|
||||||
|
not in nodes_provider_networks
|
||||||
|
):
|
||||||
|
nodes_provider_networks[
|
||||||
|
circuit_termination.provider_network.pk
|
||||||
|
] = circuit_termination.provider_network
|
||||||
|
|
||||||
|
if bool(termination_a) and bool(termination_b):
|
||||||
|
circuit_model = {
|
||||||
|
"provider_name": circuit_termination.circuit.provider.name
|
||||||
|
}
|
||||||
|
edge_ids += 1
|
||||||
|
edges.append(
|
||||||
|
create_edge(
|
||||||
|
edge_id=edge_ids,
|
||||||
|
circuit=circuit_model,
|
||||||
|
termination_a=termination_a,
|
||||||
|
termination_b=termination_b,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
circuit_has_connections = False
|
||||||
|
for termination in [
|
||||||
|
circuit_termination.cable.a_terminations[0],
|
||||||
|
circuit_termination.cable.b_terminations[0],
|
||||||
|
]:
|
||||||
|
if not isinstance(termination, CircuitTermination):
|
||||||
|
if (
|
||||||
|
termination.device_id not in nodes_devices
|
||||||
|
and termination.device_id in device_ids
|
||||||
|
):
|
||||||
|
nodes_devices[termination.device_id] = termination.device
|
||||||
|
circuit_has_connections = True
|
||||||
|
else:
|
||||||
|
if termination.device_id in device_ids:
|
||||||
|
circuit_has_connections = True
|
||||||
|
|
||||||
|
if circuit_has_connections and hide_unconnected:
|
||||||
|
if circuit_termination.circuit_id not in nodes_circuits:
|
||||||
|
nodes_circuits[
|
||||||
|
circuit_termination.circuit.pk
|
||||||
|
] = circuit_termination.circuit
|
||||||
|
|
||||||
|
for d in nodes_circuits.values():
|
||||||
|
nodes.append(create_node(d, save_coords))
|
||||||
|
|
||||||
|
links: QuerySet[CableTermination] = CableTermination.objects.filter(
|
||||||
|
Q(_device_id__in=device_ids)
|
||||||
|
).select_related("termination_type")
|
||||||
|
|
||||||
|
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
|
||||||
|
Q(_interface_a_device_id__in=device_ids)
|
||||||
|
& Q(_interface_b_device_id__in=device_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
if show_power:
|
||||||
|
power_panels_ids = PowerPanel.objects.filter(
|
||||||
|
Q(site_id__in=site_ids)
|
||||||
|
).values_list("pk", flat=True)
|
||||||
|
power_feeds: QuerySet[PowerFeed] = PowerFeed.objects.filter(
|
||||||
|
Q(power_panel_id__in=power_panels_ids)
|
||||||
|
)
|
||||||
|
|
||||||
|
for power_feed in power_feeds:
|
||||||
|
if not hide_unconnected or (
|
||||||
|
hide_unconnected and power_feed.cable_id is not None
|
||||||
|
):
|
||||||
|
if power_feed.power_panel_id not in nodes_powerpanel:
|
||||||
|
nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel
|
||||||
|
|
||||||
|
power_link_name = ""
|
||||||
|
if power_feed.pk not in nodes_powerfeed:
|
||||||
|
if hide_unconnected:
|
||||||
|
if power_feed.link_peers[0].device_id in device_ids:
|
||||||
|
nodes_powerfeed[power_feed.pk] = power_feed
|
||||||
|
power_link_name = power_feed.link_peers[0].name
|
||||||
|
else:
|
||||||
|
nodes_powerfeed[power_feed.pk] = power_feed
|
||||||
|
|
||||||
|
edge_ids += 1
|
||||||
|
termination_a = {
|
||||||
|
"termination_name": power_feed.power_panel.name,
|
||||||
|
"termination_device_name": "",
|
||||||
|
"device_id": f"p{power_feed.power_panel_id}",
|
||||||
|
}
|
||||||
|
termination_b = {
|
||||||
|
"termination_name": power_feed.name,
|
||||||
|
"termination_device_name": power_link_name,
|
||||||
|
"device_id": f"f{power_feed.pk}",
|
||||||
|
}
|
||||||
|
edges.append(
|
||||||
|
create_edge(
|
||||||
|
edge_id=edge_ids,
|
||||||
|
termination_a=termination_a,
|
||||||
|
termination_b=termination_b,
|
||||||
|
power=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if power_feed.cable_id is not None:
|
||||||
|
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
|
||||||
|
|
||||||
|
for d in nodes_powerfeed.values():
|
||||||
|
nodes.append(create_node(d, save_coords))
|
||||||
|
|
||||||
|
for d in nodes_powerpanel.values():
|
||||||
|
nodes.append(create_node(d, save_coords))
|
||||||
|
|
||||||
for link in links:
|
for link in links:
|
||||||
|
if link.termination_type.name in ignore_cable_type:
|
||||||
if link.termination_a_type.name in ignore_cable_type or link.termination_b_type.name in ignore_cable_type \
|
|
||||||
or link.id in cable_ids:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
a_is_path_endoint = isinstance(link.termination_a, PathEndpoint)
|
|
||||||
b_is_path_endoint = isinstance(link.termination_b, PathEndpoint)
|
|
||||||
|
|
||||||
if not end2end_connections or (not a_is_path_endoint and not b_is_path_endoint):
|
# Normal device cables
|
||||||
|
if link.termination_type.name in supported_termination_types:
|
||||||
if isinstance(link.termination_a, CircuitTermination):
|
complete_link = False
|
||||||
if enable_circuit_terminations and link.termination_a.circuit.id not in circuit_ids:
|
if link.cable_end == "A":
|
||||||
circuit = link.termination_a.circuit
|
if link.cable_id not in cable_ids:
|
||||||
circuit_ids.append(circuit.id)
|
cable_ids[link.cable_id] = {}
|
||||||
cable_ids.append(link.id)
|
else:
|
||||||
|
if "B" in cable_ids[link.cable_id]:
|
||||||
path_destination = circuit.termination_z if link.termination_a.term_side == "A" else circuit.termination_a
|
if cable_ids[link.cable_id]["B"] is not None:
|
||||||
|
complete_link = True
|
||||||
if path_destination is not None and path_destination.provider_network is None:
|
elif link.cable_end == "B":
|
||||||
# ProviderNetwork not supported at the moment
|
if link.cable_id not in cable_ids:
|
||||||
# $path_destination.cable would be none : there is no cable between a CircuitTermination and ProviderNetwork
|
cable_ids[link.cable_id] = {}
|
||||||
|
else:
|
||||||
if not hasattr(link.termination_b, 'device'):
|
if "A" in cable_ids[link.cable_id]:
|
||||||
#CircuitTermination B Missing Device
|
if cable_ids[link.cable_id]["A"] is not None:
|
||||||
continue
|
complete_link = True
|
||||||
|
|
||||||
origin_device = link.termination_b.device
|
|
||||||
destination_device = path_destination.cable.termination_b.device
|
|
||||||
|
|
||||||
if origin_device.id in device_ids and destination_device.id in device_ids:
|
|
||||||
if origin_device.id not in nodes_devices:
|
|
||||||
nodes_devices[origin_device.id] = origin_device
|
|
||||||
if destination_device.id not in nodes_devices:
|
|
||||||
nodes_devices[destination_device.id] = destination_device
|
|
||||||
|
|
||||||
edge_ids += 1
|
|
||||||
edges.append(create_edge(edge_ids, link, link.termination_b, path_destination.cable.termination_b, [circuit.cid], circuit))
|
|
||||||
|
|
||||||
elif link.termination_a.device.id in device_ids and link.termination_b.device.id in device_ids:
|
|
||||||
|
|
||||||
if link.termination_a.device.id not in nodes_devices:
|
|
||||||
nodes_devices[link.termination_a.device.id] = link.termination_a.device
|
|
||||||
if link.termination_b.device.id not in nodes_devices:
|
|
||||||
nodes_devices[link.termination_b.device.id] = link.termination_b.device
|
|
||||||
|
|
||||||
cable_ids.append(link.id)
|
|
||||||
edge_ids += 1
|
|
||||||
edges.append(create_edge(edge_ids, link, link.termination_a, link.termination_b))
|
|
||||||
else:
|
|
||||||
# termination_a can be a CircuitTermination (no trace()) while termination_b is a PathEndpoint
|
|
||||||
# If so, we swap them so we can follow the path using PathEndpoint#trace()
|
|
||||||
path_start = link.termination_a if a_is_path_endoint else link.termination_b
|
|
||||||
if path_start.device is not None and path_start.device.id not in device_ids:
|
|
||||||
# device not in queryset, skip
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Ignore incomplete path when in end-to-end connection mode
|
|
||||||
if path_start.path is not None:
|
|
||||||
if not path_start.path.is_active or path_start.path.is_split:
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
continue
|
print("Unkown cable end")
|
||||||
|
cable_ids[link.cable_id][link.cable_end] = link
|
||||||
|
|
||||||
path_destination = path_start.path.destination
|
if complete_link:
|
||||||
if hasattr(path_destination, "device") and path_destination.device is not None and path_destination.device.id not in device_ids:
|
edge_ids += 1
|
||||||
# device not in queryset, skip
|
if isinstance(cable_ids[link.cable_id]["B"], CableTermination):
|
||||||
continue
|
if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices:
|
||||||
|
nodes_devices[
|
||||||
|
cable_ids[link.cable_id]["B"]._device_id
|
||||||
|
] = cable_ids[link.cable_id]["B"].termination.device
|
||||||
|
termination_b = {
|
||||||
|
"termination_name": cable_ids[link.cable_id][
|
||||||
|
"B"
|
||||||
|
].termination.name,
|
||||||
|
"termination_device_name": cable_ids[link.cable_id][
|
||||||
|
"B"
|
||||||
|
].termination.device.name,
|
||||||
|
"device_id": cable_ids[link.cable_id][
|
||||||
|
"B"
|
||||||
|
].termination.device_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
termination_b = cable_ids[link.cable_id]["B"]
|
||||||
|
|
||||||
valid_path = False
|
if isinstance(cable_ids[link.cable_id]["A"], CableTermination):
|
||||||
|
if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices:
|
||||||
|
nodes_devices[
|
||||||
|
cable_ids[link.cable_id]["A"]._device_id
|
||||||
|
] = cable_ids[link.cable_id]["A"].termination.device
|
||||||
|
termination_a = {
|
||||||
|
"termination_name": cable_ids[link.cable_id][
|
||||||
|
"A"
|
||||||
|
].termination.name,
|
||||||
|
"termination_device_name": cable_ids[link.cable_id][
|
||||||
|
"A"
|
||||||
|
].termination.device.name,
|
||||||
|
"device_id": cable_ids[link.cable_id][
|
||||||
|
"A"
|
||||||
|
].termination.device_id,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
termination_a = cable_ids[link.cable_id]["A"]
|
||||||
|
|
||||||
# variables needed for trace() browsing
|
edges.append(
|
||||||
origin = None
|
create_edge(
|
||||||
circuit = None
|
edge_id=edge_ids,
|
||||||
tmp_path = []
|
cable=link.cable,
|
||||||
|
termination_a=termination_a,
|
||||||
|
termination_b=termination_b,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# trace() : Return the path as a list of three-tuples (A termination, cable, B termination)
|
for wlan_link in wlan_links:
|
||||||
# TODO device qui ne s"affichent pas si hide_unconnected
|
if wlan_link.interface_a.device_id not in nodes_devices:
|
||||||
for (termination_a, cable, termination_b) in path_start.trace():
|
nodes_devices[
|
||||||
|
wlan_link.interface_a.device.pk
|
||||||
if origin is None:
|
] = wlan_link.interface_a.device
|
||||||
# New part of the link
|
if wlan_link.interface_b.device_id not in nodes_devices:
|
||||||
origin = termination_a
|
nodes_devices[
|
||||||
circuit = None
|
wlan_link.interface_b.device.pk
|
||||||
tmp_path = []
|
] = wlan_link.interface_b.device
|
||||||
|
|
||||||
if isinstance(termination_b, ProviderNetwork):
|
termination_a = {
|
||||||
# ProviderNetwork not supported at the moment
|
"termination_name": wlan_link.interface_a.name,
|
||||||
# It would need to manage several kind of nodes (Device, ProviderNetwork)
|
"termination_device_name": wlan_link.interface_a.device.name,
|
||||||
# and maps javascript nodes ID with Devices IDs and ProviderNetworks IDs
|
"device_id": wlan_link.interface_a.device_id,
|
||||||
# When $termination_b is a ProviderNetwork instance, $cable will be None
|
}
|
||||||
break
|
termination_b = {
|
||||||
|
"termination_name": wlan_link.interface_b.name,
|
||||||
|
"termination_device_name": wlan_link.interface_b.device.name,
|
||||||
|
"device_id": wlan_link.interface_b.device_id,
|
||||||
|
}
|
||||||
|
wireless = {"ssid": wlan_link.ssid}
|
||||||
|
|
||||||
if cable is not None and (cable.termination_a_type in ignore_cable_type or cable.termination_b_type in ignore_cable_type):
|
edge_ids += 1
|
||||||
# Ignore this path
|
edges.append(
|
||||||
break
|
create_edge(
|
||||||
|
edge_id=edge_ids,
|
||||||
|
termination_a=termination_a,
|
||||||
|
termination_b=termination_b,
|
||||||
|
wireless=wireless,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(termination_b, CircuitTermination):
|
|
||||||
if enable_circuit_terminations:
|
|
||||||
circuit = termination_b.circuit
|
|
||||||
tmp_path.append(circuit.cid)
|
|
||||||
if circuit.id not in circuit_ids:
|
|
||||||
circuit_ids.append(circuit.id)
|
|
||||||
else:
|
|
||||||
# Ignore this path
|
|
||||||
break
|
|
||||||
elif end2end_connections and termination_b != path_destination \
|
|
||||||
and termination_b.device.device_role.id not in intermediate_dev_role_ids \
|
|
||||||
and termination_b.device.id not in device_ids:
|
|
||||||
# Skip this intermediate device, origin device is keep in $origin for next iteration
|
|
||||||
tmp_path.append(termination_b.device.name)
|
|
||||||
elif cable is not None and termination_b is not None:
|
|
||||||
if cable.id not in cable_ids:
|
|
||||||
# New part of the link we want to display
|
|
||||||
cable_ids.append(cable.id)
|
|
||||||
edge_ids += 1
|
|
||||||
edges.append(create_edge(edge_ids, cable, origin, termination_b, tmp_path, circuit))
|
|
||||||
|
|
||||||
if not valid_path:
|
|
||||||
valid_path = True
|
|
||||||
|
|
||||||
if termination_b.device.id not in nodes_devices and \
|
|
||||||
(termination_b.device.device_role.id in intermediate_dev_role_ids or termination_b.device.id in device_ids) :
|
|
||||||
nodes_devices[termination_b.device.id] = termination_b.device
|
|
||||||
# Reset for next iteration
|
|
||||||
origin = None
|
|
||||||
# endfor (trace)
|
|
||||||
|
|
||||||
if valid_path and path_start.device.id not in nodes_devices:
|
|
||||||
nodes_devices[path_start.device.id] = path_start.device
|
|
||||||
|
|
||||||
# endfor (link)
|
|
||||||
|
|
||||||
for qs_device in queryset:
|
for qs_device in queryset:
|
||||||
if qs_device.id not in nodes_devices and not hide_unconnected:
|
if qs_device.pk not in nodes_devices and not hide_unconnected:
|
||||||
nodes_devices[qs_device.id] = qs_device
|
nodes_devices[qs_device.pk] = qs_device
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
results["nodes"] = [create_node(d) for d in nodes_devices.values()]
|
|
||||||
|
for d in nodes_devices.values():
|
||||||
|
nodes.append(create_node(d, save_coords))
|
||||||
|
|
||||||
|
results["nodes"] = nodes
|
||||||
results["edges"] = edges
|
results["edges"] = edges
|
||||||
return results
|
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")
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Show the home page
|
Show the home page
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
self.filterset = DeviceFilterSet
|
self.filterset = DeviceFilterSet
|
||||||
self.queryset = Device.objects.all().select_related("device_type", "device_role")
|
self.queryset = Device.objects.all().select_related(
|
||||||
|
"device_type", "device_role"
|
||||||
|
)
|
||||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||||
topo_data = None
|
topo_data = None
|
||||||
|
|
||||||
if request.GET:
|
if request.GET:
|
||||||
|
save_coords = False
|
||||||
|
if "save_coords" in request.GET:
|
||||||
|
if request.GET["save_coords"] == "on":
|
||||||
|
save_coords = True
|
||||||
|
|
||||||
hide_unconnected = False
|
hide_unconnected = False
|
||||||
if "hide_unconnected" in request.GET:
|
if "hide_unconnected" in request.GET:
|
||||||
if request.GET["hide_unconnected"] == "on" :
|
if request.GET["hide_unconnected"] == "on":
|
||||||
hide_unconnected = True
|
hide_unconnected = True
|
||||||
|
|
||||||
if "intermediate_dev_role_id" in request.GET:
|
show_power = False
|
||||||
intermediate_dev_role_ids = list(map(int, request.GET.getlist("intermediate_dev_role_id")))
|
if "show_power" in request.GET:
|
||||||
else:
|
if request.GET["show_power"] == "on":
|
||||||
intermediate_dev_role_ids = []
|
show_power = True
|
||||||
|
|
||||||
|
show_circuit = False
|
||||||
|
if "show_circuit" in request.GET:
|
||||||
|
if request.GET["show_circuit"] == "on":
|
||||||
|
show_circuit = True
|
||||||
|
|
||||||
end2end_connections = False
|
|
||||||
if "end2end_connections" in request.GET:
|
|
||||||
if request.GET["end2end_connections"] == "on" :
|
|
||||||
end2end_connections = True
|
|
||||||
|
|
||||||
if "draw_init" in request.GET:
|
if "draw_init" in request.GET:
|
||||||
if request.GET["draw_init"].lower() == "true":
|
if request.GET["draw_init"].lower() == "true":
|
||||||
topo_data = get_topology_data(self.queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections)
|
topo_data = get_topology_data(
|
||||||
|
self.queryset,
|
||||||
|
hide_unconnected,
|
||||||
|
save_coords,
|
||||||
|
show_circuit,
|
||||||
|
show_power,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
topo_data = get_topology_data(self.queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections)
|
topo_data = get_topology_data(
|
||||||
|
self.queryset,
|
||||||
|
hide_unconnected,
|
||||||
|
save_coords,
|
||||||
|
show_circuit,
|
||||||
|
show_power,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||||
preselected_intermediate_dev_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_intermediate_dev_roles"]
|
"preselected_device_roles"
|
||||||
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
|
]
|
||||||
|
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||||
|
"preselected_tags"
|
||||||
|
]
|
||||||
|
always_save_coordinates = bool(
|
||||||
|
settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||||
|
"always_save_coordinates"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
q_device_role_id = DeviceRole.objects.filter(name__in=preselected_device_roles).values_list("id", flat=True)
|
q_device_role_id = DeviceRole.objects.filter(
|
||||||
q_intermediate_dev_role_id = DeviceRole.objects.filter(name__in=preselected_intermediate_dev_roles).values_list("id", flat=True)
|
name__in=preselected_device_roles
|
||||||
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list("name", flat=True)
|
).values_list("id", flat=True)
|
||||||
|
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list(
|
||||||
|
"name", flat=True
|
||||||
|
)
|
||||||
|
|
||||||
q = QueryDict(mutable=True)
|
q = QueryDict(mutable=True)
|
||||||
q.setlist("device_role_id", list(q_device_role_id))
|
q.setlist("device_role_id", list(q_device_role_id))
|
||||||
q.setlist("intermediate_dev_role_id", list(q_intermediate_dev_role_id))
|
|
||||||
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"
|
||||||
|
]
|
||||||
|
if always_save_coordinates:
|
||||||
|
q["save_coords"] = "on"
|
||||||
query_string = q.urlencode()
|
query_string = q.urlencode()
|
||||||
return HttpResponseRedirect(request.path + "?" + query_string)
|
return HttpResponseRedirect(f"{request.path}?{query_string}")
|
||||||
|
|
||||||
return render(request, "netbox_topology_views/index.html" , {
|
return render(
|
||||||
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
|
request,
|
||||||
"topology_data": json.dumps(topo_data)
|
"netbox_topology_views/index.html",
|
||||||
}
|
{
|
||||||
|
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
|
||||||
|
"topology_data": json.dumps(topo_data),
|
||||||
|
"broken_image": find_image_url("role-unknown"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG = settings.PLUGINS_CONFIG["netbox_topology_views"]
|
||||||
|
ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
|
||||||
|
|
||||||
|
|
||||||
|
class TopologyImagesView(PermissionRequiredMixin, View):
|
||||||
|
permission_required = (
|
||||||
|
"dcim.view_site",
|
||||||
|
"dcim.view_device_role",
|
||||||
|
"dcim.add_device_role",
|
||||||
|
"dcim.change_device_role",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest):
|
||||||
|
images = [
|
||||||
|
{"url": image_static_url(image), "title": image.stem}
|
||||||
|
for image in CONF_IMAGE_DIR.iterdir()
|
||||||
|
]
|
||||||
|
|
||||||
|
roles = reduce(
|
||||||
|
lambda acc, cur: {
|
||||||
|
**acc,
|
||||||
|
cur.name: {
|
||||||
|
"id": cur.pk,
|
||||||
|
"name": cur.name,
|
||||||
|
"slug": cur.slug,
|
||||||
|
"image": find_image_url(cur.slug),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DeviceRole.objects.all(),
|
||||||
|
dict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for additional_role in ADDITIONAL_ROLES:
|
||||||
|
cur = get_model_role(additional_role)
|
||||||
|
ct = ContentType.objects.get_for_model(additional_role).pk
|
||||||
|
|
||||||
|
roles[cur.name] = {
|
||||||
|
"id": f"ct{ct}",
|
||||||
|
"name": cur.name,
|
||||||
|
"slug": cur.slug,
|
||||||
|
"image": find_image_url(cur.slug),
|
||||||
|
}
|
||||||
|
|
||||||
|
role_images = RoleImage.objects.all()
|
||||||
|
|
||||||
|
for role_image in role_images:
|
||||||
|
roles[role_image.role.name]["image"] = role_image.get_image_url()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"netbox_topology_views/images.html",
|
||||||
|
{
|
||||||
|
"roles": sorted(list(roles.values()), key=lambda r: r["name"]),
|
||||||
|
"images": images,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Generated
-4781
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"name": "netbox_topology_views",
|
|
||||||
"version": "2.0.2",
|
|
||||||
"scripts": {
|
|
||||||
"resources": "gulp build",
|
|
||||||
"resources_dev": "gulp build_dev"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"vis-data": "^7.1.2",
|
|
||||||
"vis-network": "^9.1.0",
|
|
||||||
"vis-util": "^5.0.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"gulp": "^4.0.2",
|
|
||||||
"gulp-clean-css": "^4.3.0",
|
|
||||||
"gulp-concat": "^2.6.1",
|
|
||||||
"gulp-sass": "^4.1.0",
|
|
||||||
"gulp-uglify": "^3.0.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@egjs/hammerjs": "^2.0.0",
|
|
||||||
"uuid": "^8.0.0",
|
|
||||||
"keycharm": "^0.4.0",
|
|
||||||
"timsort": "^0.3.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
djangorestframework
|
||||||
|
django-filter
|
||||||
|
black
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
from setuptools import setup, find_packages
|
from pathlib import Path
|
||||||
|
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
readme = Path(__file__).parent / "README.md"
|
||||||
|
long_description = readme.read_text()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='netbox-topology-views',
|
name="netbox-topology-views",
|
||||||
version='2.0.2',
|
version="3.1.0",
|
||||||
description='An NetBox plugin to create Topology maps',
|
description="An NetBox plugin to create Topology maps",
|
||||||
url='https://github.com/mattieserver/netbox-topology-views',
|
long_description=long_description,
|
||||||
author='Mattijs Vanhaverbeke',
|
long_description_content_type="text/markdown",
|
||||||
license='Apache 2.0',
|
url="https://github.com/mattieserver/netbox-topology-views",
|
||||||
|
author="Mattijs Vanhaverbeke",
|
||||||
|
license="Apache 2.0",
|
||||||
install_requires=[],
|
install_requires=[],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
keywords=["netbox-plugin"],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python',
|
"Programming Language :: Python",
|
||||||
'Programming Language :: Python :: 3',
|
"Programming Language :: Python :: 3",
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
],
|
],
|
||||||
project_urls={
|
project_urls={
|
||||||
'Source': 'https://github.com/mattieserver/netbox-topology-views',
|
"Source": "https://github.com/mattieserver/netbox-topology-views",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user