Compare commits

...
38 Commits
Author SHA1 Message Date
mattieserver 41d6afc336 bump version (#222) 2022-12-18 18:50:58 +01:00
mattieserver 2901dbb5c7 add check for python3.9 (#221) 2022-12-18 18:45:49 +01:00
mattieserverandMario 37122cd76c initial update of docs (#213)
* initial update of docs

* updated docs

* Update dev_setup/README.md

Co-authored-by: Mario <github@franzbonenkamp.de>

* Update dev_setup/README.md

Co-authored-by: Mario <github@franzbonenkamp.de>

* Update dev_setup/README.md

Co-authored-by: Mario <github@franzbonenkamp.de>

* added docker / native install for psql

* Update dev_setup/README.md

Co-authored-by: Mario <github@franzbonenkamp.de>

Co-authored-by: Mario <github@franzbonenkamp.de>
2022-12-18 18:21:12 +01:00
Renato Almeida de OliveiraandMattijs Vanhaverbeke 7173564ea9 Topology button in Site View (#220)
* Topology button in Site View

* Add mdi icons to Topology and Close Buttons

* Fix buttons tag and class

* fixed drag & site filter

Co-authored-by: Mattijs Vanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
2022-12-18 18:15:48 +01:00
Mario be402b1b5c Closes #210: Issue templates (#211)
* Create bug_report.yaml

* Create feature_request.yaml

* Create config.yml
2022-12-11 12:29:36 +01:00
mattieserver f187e75fff bump version (#200) 2022-12-09 13:52:44 +01:00
mattieserver c6df846bde changed api call path (#198) 2022-12-09 13:46:30 +01:00
Victor Gorchilov 52c2611e9d Feature: Add customisable icons in topology (#193)
* [up] add config for custom images, better js

* [up] use bundler to bundle js

* [fix] remove domcontentloaded since defer is used

* [fix] formatting with black

* [fix] images drf api

* [up] add support for custom icons on additional roles: power panel, power feed, circuit

* [up] optimize topology generation by using `_id` when trying to access pk of related object

* [fix] kebab case instead of slugification

* [fix] use empty queryset in api views & change coord save logic
2022-12-09 13:40:39 +01:00
Robin Schneider 5c87694b87 docs: Recommend to set custom field coordinates to hidden (#180)
Changing the coordinates manually would be very tedious. Users are not
expected to do this and to prevent them from coming up with this idea in
the first place, set the custom field to hidden.
2022-11-03 13:42:51 +01:00
Roy Vegard Ovesen 94f5314649 Filter by rack (#175)
Fixes #174
2022-10-09 19:12:59 +02:00
mattieserver 32042ff2cc added multiselect (#171) 2022-10-04 10:16:20 +02:00
Mattijs Vanhaverbeke 9ce13cd1d2 bump version 2022-09-16 16:14:42 +02:00
mattieserver b4e2f2418e added extra checks (#166) 2022-09-16 16:13:32 +02:00
mattieserver 5563e3c084 added device filter option (#165) 2022-09-16 16:07:14 +02:00
mattieserver 77a8a2c0d6 Removed print 2022-09-16 14:52:00 +02:00
mattieserver 010179742c 159 fix power filter (#164)
* fix site_ids var

* fixed power cable name + change power filter name
2022-09-16 14:50:24 +02:00
mattieserver f2bbaed317 added console cable support (#157)
* added console cable support

* removed print
2022-09-14 14:50:10 +02:00
mattieserver 8c56f21f16 Update README.md 2022-09-13 08:46:05 +02:00
mattieserver eb627bdedc Update README.md 2022-09-12 18:36:27 +02:00
mattieserver 291c033e34 Update README.md 2022-09-12 18:23:33 +02:00
mattieserver 1f389b3faa Update README.md 2022-09-12 18:20:18 +02:00
Mattijs Vanhaverbeke 3d3ddaa7c4 bump version 2022-09-08 12:29:29 +02:00
mattieserver 4ea2097429 bump version 2022-09-02 15:13:22 +02:00
mattieserver cfaba462d5 removed unused import (#149) 2022-09-02 15:12:36 +02:00
Mattijs Vanhaverbeke 5e2f1cca57 added info about new icons 2022-09-01 19:10:15 +02:00
Mattijs Vanhaverbeke b2e2a4c781 bump version 2022-09-01 18:48:22 +02:00
mattieserver 318b8495b7 Netbox v3.3 Support (#145)
* init

* added circuit (or at least part of)

* refactor circuits + fix js

* small changes

* small changes

* added power + rework settings

* small changes
2022-09-01 18:44:03 +02:00
mattieserver 5e1d5d2937 added always_save_coordinates (#144) 2022-08-24 14:07:26 +02:00
Andrea DaineseandMattijs Vanhaverbeke 0b6df3bd28 disable physics if not saving coords (#139)
* initial commit

* keeps x/y when not saving coords

Co-authored-by: Mattijs Vanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
2022-08-05 17:53:15 +02:00
mattieserver af1666b3ba move to esbuild & yarn (#138)
* move to esbuild & yarn

* remove gulpfile

* cleanup unused ref

* updated gitignore
2022-08-03 17:29:15 +02:00
Mattijs Vanhaverbeke 31a8c756e2 package info 2022-08-03 14:07:51 +02:00
Mattijs Vanhaverbeke ce39c98396 bump version 2022-08-03 12:07:58 +02:00
mattieserver faf156a6b2 133 enhancement bubbletext (#136)
* added extra fields to popup

* open new tab with device info
2022-08-03 11:46:37 +02:00
mattieserver 1b36b9e7df added extra check (#135) 2022-08-03 10:59:02 +02:00
mattieserver 652a44437e Create SECURITY.md 2022-07-29 09:33:15 +02:00
mattieserver 76141e70ce update gh actions 2022-06-30 22:47:55 +02:00
mattieserver 76b4f6ac9a Create docker-publish.yml 2022-06-30 22:40:18 +02:00
mattieserver 953d7480af add dockerfile (#130) 2022-06-30 22:39:18 +02:00
56 changed files with 2962 additions and 5438 deletions
+52
View File
@@ -0,0 +1,52 @@
---
name: 🐛 Bug Report
description: Report a reproducible bug in the current release of NetBox
labels: ["type: bug"]
body:
- type: markdown
attributes:
value: >
**NOTE:** This form is only for reporting _reproducible bugs_. If you're having
trouble with installation or just looking for assistance with using NetBox
Topology Views, please visit our
[discussion forum](https://github.com/mattieserver/netbox-topology-views/discussions) instead.
- type: input
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.9
validations:
required: true
- type: input
attributes:
label: Topology Views version
description: What version of NetBox Topology Views are you currently running?
placeholder: v3.1.0
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: >
Describe in detail the exact steps that someone else can take to
reproduce this bug using the current stable release of NetBox Topology Views.
placeholder: |
1. Click on "create widget"
2. Set foo to 12 and bar to G
3. Click the "create" button
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: A new widget should have been created with the specified attributes
validations:
required: true
- type: textarea
attributes:
label: Observed Behavior
description: What happened instead?
placeholder: A TypeError exception was raised
validations:
required: true
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ❓ Discussion
url: https://github.com/mattieserver/netbox-topology-views/discussions
about: "If you're just looking for help, try starting a discussion instead"
@@ -0,0 +1,58 @@
---
name: ✨ Feature Request
description: Propose a new NetBox Topology Views feature or enhancement
labels: ["type: feature"]
body:
- type: markdown
attributes:
value: >
**NOTE:** This form is only for submitting well-formed proposals to extend or modify
NetBox Topology Views in some way. If you're trying to solve a problem but can't figure
out how, or if you still need time to work on the details of a proposed new feature,
please start a [discussion](https://github.com/mattieserver/netbox-topology-views/discussions) instead.
- type: input
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.9
validations:
required: true
- type: input
attributes:
label: Topology Views version
description: What version of NetBox Topology Views are you currently running?
placeholder: v3.1.0
validations:
required: true
- type: dropdown
attributes:
label: Feature type
options:
- New functionality
- Change to existing functionality
validations:
required: true
- type: textarea
attributes:
label: Proposed functionality
description: >
Describe in detail the new feature or behavior you are proposing. Include any specific changes
to work flows and/or the user interface. The more detail you provide here, the
greater chance your proposal has of being discussed. Feature requests which don't include an
actionable implementation plan will be rejected.
validations:
required: true
- type: textarea
attributes:
label: Use case
description: >
Explain how adding this functionality would benefit NetBox Topology Views users. What need does it address?
validations:
required: true
- type: textarea
attributes:
label: External dependencies
description: >
List any new dependencies on external libraries or services that this new feature would
introduce. For example, does the proposal require the installation of a new Python package?
(Not all new features introduce new dependencies.)
+93
View File
@@ -0,0 +1,93 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '35 10 * * *'
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25
with:
cosign-release: 'v1.9.0'
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
COSIGN_EXPERIMENTAL: "true"
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
+11 -11
View File
@@ -5,7 +5,7 @@ name: Upload Python Package
on:
release:
types: [created]
types: [published]
jobs:
deploy:
@@ -13,19 +13,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
+2 -1
View File
@@ -128,4 +128,5 @@ dmypy.json
# Pyre type checker
.pyre/
node_modules
node_modules
app.js.map
+4
View File
@@ -0,0 +1,4 @@
FROM netboxcommunity/netbox:latest
COPY ./docker/plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
+46 -25
View File
@@ -29,25 +29,38 @@ Once installed, the plugin needs to be enabled in your `configuration.py`
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
| netbox version | netbox-topology-views version |
| ------------- |-------------|
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
| >= 2.11.1 | >= v0.5.3 |
| >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 |
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| >= 3.3.0 | >= v3.0.0 |
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
| >= 2.11.1 | >= v0.5.3 |
| >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 |
### Custom field: coordinates
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
@@ -57,28 +70,25 @@ Example:
```
PLUGINS_CONFIG = {
'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 |
| ------------- |-------------| -----|
| device_img |['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage,wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'] | The slug of the device roles that you have a image for. |
| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive|
| preselected_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 |
| ignore_cable_type | ['power outlet', 'power port'] | The cable types that you want to ignore in the views |
| preselected_tags | '[]' | The name of tags you want to preload |
| 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) |
| Setting | Default value | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 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 |
| 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 | [] | The cable types that you want to ignore in the views |
| preselected_tags | [] | The name of tags you want to preload |
| 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
You upload you own custom images to the netbox static dir (`static/netbox_topology_views/img/`).
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.
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.
## 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 `python3 manage.py migrate netbox_topology_views`
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
+ extras | tag | can view tag
+ 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)
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| > 2.0 | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Please create an issue when you encounter a Security issue.
+138 -17
View File
@@ -1,22 +1,143 @@
# Setup Dev
+ install docker
+ install docker compose
+ install npm
+ clone netbox repo
+ clone netbox_topology_views repo
+ create venv: `/usr/bin/python3 -m venv venv`
+ activate venv: `source venv/bin/activate`
+ install deps: `pip3 install -r requirements.txt`
+ install build tools: `python3 -m pip install --upgrade build setuptools wheel`
We will assume that you work in the folder `~/code/netbox_plugin/`, change this if you use another folder for your projects/code.
## Setup native prerequisites
**_NOTE:_** these requirements are needed if you want install Postgresql & Redis native. Check the steps for docker & docker-compose if you don't want this.
Make sure that you have already installed Postgresql and Redis. Also make sure that the database has already been created and privileges has been granted as described in the NetBox installation instructions at https://docs.netbox.dev/en/stable/installation/.
In short:
```
sudo apt update
sudo apt install -y postgresql
sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo -u postgres psql
CREATE DATABASE netbox;
CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
GRANT ALL PRIVILEGES ON DATABASE netbox TO netbox;
sudo apt install -y redis-server
```
## Setup docker prerequisites
**_NOTE:_** if you used the 'native' prerequisites you can skip these steps below
### Docker
This depends on your OS or how you want to install docker.
See https://docs.docker.com/engine/install/ to find the install steps.
### Docker compose
This also depends on your OS, see the link below:
https://docs.docker.com/compose/install/linux/
## Create the base folder
```
mkdir ~/code/netbox_plugin/
cd ~/code/netbox_plugin/
```
## Install requirements
### Python + deps
You will need the following packages installed:
+ python3-venv
+ python3
Install them with your OS package manager (apt, yum, ...)
### NPM (with nvm)
Follow the steps on the repo below to install npm:
https://github.com/nvm-sh/nvm#installing-and-updating
## Create venv
```
python3 -m venv ~/code/netbox_plugin/.venvs/netbox
source ~/code/netbox_plugin/.venvs/netbox/bin/activate
```
## Setup netbox
### Clone netbox repo
```
cd ~/code/netbox_plugin/
git clone git@github.com:netbox-community/netbox.git netbox
```
### Install deps
```
cd netbox
python3 -m pip install -r requirements.txt
```
## Setup plugin
### Clone netbox-topology-views repo
```
cd ~/code/netbox_plugin/
git clone git@github.com:mattieserver/netbox-topology-views.git plugin
```
### Install deps
```
python3 -m pip install --upgrade build setuptools wheel
```
### Copy netbox config file
```
cd ~/code/netbox_plugin/
cp plugin/dev_setup/configuration.py netbox/netbox/netbox/configuration.py
```
## Start netbox
### Start docker compose
**_NOTE:_** if you used the 'native' prerequisites you can skip to 'Start netbox'
```
cd ~/code/netbox_plugin/
cd plugin/dev_setup
docker-compose up -d
```
### Start netbox
```
cd ~/code/netbox_plugin/
source ~/code/netbox_plugin/.venvs/netbox/bin/activate
cd netbox/netbox/
python3 manage.py migrate
python3 manage.py runserver
```
You might need to create a superuser if you want to start with an empty database: `python3 manage.py createsuperuser`
# Build
+ build the package: `python3 setup.py develop`
+ build the resources `npm run resources`
# Test
+ copy configuration.py from dev_setup to the netbox/netbox/ folder
+ run in the netbox repo: `python3 netbox/manage.py migrate`
+ (first time only) run in the netbox repo: `python3 netbox/manage.py createsuperuser`
+ run in the netbox repo: `python3 netbox/manage.py runserver`
+ browse to http://127.0.0.1:8000
## Build the plugin
```
cd ~/code/netbox_plugin/
source ~/code/netbox_plugin/.venvs/netbox/bin/activate
cd plugin/
python3 setup.py develop
```
## Build javascript
```
cd plugin/netbox_topology_views/static_dev
npm install
npm run bundle
```
### Test
You can now browse to http://localhost:8000.
The plugin should be available in the menu.
# Import demo data
If you want to add demo data instead of your own data, there's an official Repository you can use. The following example shows how to add demo data for NetBox v3.4. You have to use the json file according to the version you are working on.
```
~/code/netbox_plugin/
git clone git@github.com:netbox-community/netbox-demo-data.git demodata
source ~/code/netbox_plugin/.venvs/netbox/bin/activate
cd netbox/netbox/
python3 manage.py loaddata -v 3 ~/src/netbox-demo-data/netbox-demo-v3.4.json
```
Please note that the demo data adds a superuser with credentials admin/admin.
+1
View File
@@ -0,0 +1 @@
netbox-topology-views
-78
View File
@@ -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);
+27 -16
View File
@@ -1,24 +1,35 @@
from extras.plugins import PluginConfig
class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render topology maps'
version = '2.0.1'
author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com'
base_url = 'netbox_topology_views'
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "3.1.1"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
required_settings = []
default_settings = {
'preselected_device_roles': ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'],
'preselected_intermediate_dev_roles' : [],
'end2end_connections': False,
'ignore_cable_type': ['power outlet','power port'],
'device_img': ['access-switch', 'core-switch', 'firewall', 'router', 'distribution-switch', 'backup', 'storage', 'wan-network', 'wireless-ap', 'server', 'internal-switch', 'isp-cpe-material', 'non-racked-devices', 'power-units'],
'allow_coordinates_saving': False,
'preselected_tags' : [],
'enable_circuit_terminations': False,
'draw_default_layout': False
"preselected_device_roles": [
"Firewall",
"Router",
"Distribution Switch",
"Core Switch",
"Internal Switch",
"Access Switch",
"Server",
"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
+1 -1
View File
@@ -1 +1 @@
"""REST API"""
"""REST API"""
+15 -5
View File
@@ -1,12 +1,22 @@
from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer
from dcim.models import DeviceRole, Device
from extras.models import Tag
from netbox_topology_views.models import RoleImage
class TopologyDummySerializer(ModelSerializer):
class Meta:
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")
+7 -5
View File
@@ -1,8 +1,10 @@
from rest_framework import routers
from . import views
from netbox.api.routers import NetBoxRouter
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
+115 -47
View File
@@ -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.response import Response
from django.contrib.contenttypes.models import ContentType
from rest_framework.routers import APIRootView
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from .serializers import TopologyDummySerializer
from django.conf import settings
from dcim.models import DeviceRole, Device, Cable
from circuits.models import Circuit
from extras.models import Tag
class TopologyViewsRootView(APIRootView):
def get_view_name(self):
return 'TopologyViews'
from netbox_topology_views.api.serializers import (
RoleImageSerializer,
TopologyDummySerializer,
)
from netbox_topology_views.models import RoleImage
from netbox_topology_views.utils import get_image_from_url
class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.all()
queryset = Device.objects.none()
serializer_class = TopologyDummySerializer
@action(detail=False, methods=['patch'])
@action(detail=False, methods=["patch"])
def save_coords(self, request):
results = {}
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
device_id = None
x_coord = None
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"]
if not settings.PLUGINS_CONFIG["netbox_topology_views"][
"allow_coordinates_saving"
]:
return Response({"status": "not allowed to save coords"}, status=500)
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.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
actual_device.save()
results["status"] = "saved coords"
else:
try:
actual_device.custom_field_data["coordinates"] = "%s;%s" % (x_coord,y_coord)
actual_device.save()
results["status"] = "saved coords"
except :
results["status"] = "coords custom field not created"
return Response(status=500)
actual_device = None
if device_id.startswith("c"):
device_id = device_id.lstrip("c")
actual_device = Circuit.objects.get(id=device_id)
elif device_id.startswith("p"):
device_id = device_id.lstrip("p")
actual_device = PowerPanel.objects.get(id=device_id)
elif device_id.startswith("f"):
device_id = device_id.lstrip("f")
actual_device = PowerFeed.objects.get(id=device_id)
elif device_id.isnumeric():
actual_device = Device.objects.get(id=device_id)
return Response(results)
else:
results["status"] = "not allowed to save coords"
return Response(results, status=500)
if not actual_device:
return Response({"status": "invalid node_id in body"}, status=400)
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"})
+21 -25
View File
@@ -1,59 +1,55 @@
from cProfile import label
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.models import Device, DeviceRole, Location, Rack, Region, Site
from django.db.models import Q
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.models import TenantGroup, Tenant
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
q = django_filters.CharFilter(
method='search',
label='Search',
method="search",
label="Search",
)
device_role_id = django_filters.ModelMultipleChoiceFilter(
field_name='device_role_id',
field_name="device_role_id",
queryset=DeviceRole.objects.all(),
label='Role (ID)',
label="Role (ID)",
)
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='site__region',
lookup_expr='in',
label='Region (ID)',
field_name="site__region",
lookup_expr="in",
label="Region (ID)",
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
label="Site (ID)",
)
location_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(),
field_name='location',
lookup_expr='in',
label='Location (ID)',
field_name="location",
lookup_expr="in",
label="Location (ID)",
)
rack_id = django_filters.ModelMultipleChoiceFilter(
queryset=Rack.objects.all(),
field_name="rack_id",
label="Rack (ID)",
)
status = django_filters.MultipleChoiceFilter(
choices=DeviceStatusChoices,
null_value=None
null_value=None,
)
class Meta:
model = Device
fields = ['id', 'name']
fields = ["id", "name"]
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
qs_filter = (
Q(name__icontains=value)
)
qs_filter = Q(name__icontains=value)
return queryset.filter(qs_filter)
+70 -36
View File
@@ -5,7 +5,7 @@ from django.conf import settings
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 dcim.choices import DeviceStatusChoices
@@ -13,74 +13,108 @@ from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm
from utilities.forms import (TagFilterField, DynamicModelMultipleChoiceField, MultipleChoiceField)
from utilities.forms import (
TagFilterField,
DynamicModelMultipleChoiceField,
MultipleChoiceField,
)
allow_coordinates_saving = settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
end2end = settings.PLUGINS_CONFIG["netbox_topology_views"]["end2end_connections"]
allow_coordinates_saving = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
)
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device
fieldsets = (
(None, ('q', 'hide_unconnected', 'save_coords', 'end2end_connections')),
(None, ('tenant_group_id', 'tenant_id')),
(None, ('region_id', 'site_id', 'location_id')),
(None, ('device_role_id', 'intermediate_dev_role_id')),
(None, ('tag', 'status')),
(
None,
(
"q",
"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(
queryset=Region.objects.all(),
required=False,
label=_('Region')
queryset=Region.objects.all(), required=False, label=_("Region")
)
device_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Device Role')
queryset=DeviceRole.objects.all(), required=False, label=_("Device Role")
)
end2end_connections = forms.BooleanField(
label=_("Display end-to-end connections"),
id = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False,
initial=end2end
)
intermediate_dev_role_id = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label=_('Intermediate Devices Role'),
help_text='Intermediate devices to display when using end-to-end connections mode, even if they do not match the query'
label=_("Device"),
query_params={
"location_id": "$location_id",
"region_id": "$region_id",
"site_id": "$site_id",
"role_id": "$device_role_id",
},
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
"region_id": "$region_id",
},
label=_('Site')
label=_("Site"),
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
query_params={
'region_id': '$region_id',
'site_id': '$site_id',
"region_id": "$region_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(
label=_("Hide Unconnected"),
required=False,
initial=False
label=_("Hide Unconnected"), 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(
label=_("Save Coordinates"),
required=False,
disabled=(not allow_coordinates_saving),
initial=False
)
status = MultipleChoiceField(
choices=DeviceStatusChoices,
required=False,
label=_("Device Status")
choices=DeviceStatusChoices, required=False, label=_("Device Status")
)
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'),
),
]
+93
View File
@@ -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}")
+3 -4
View File
@@ -1,9 +1,8 @@
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
from extras.plugins import PluginMenuItem
menu_items = (
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
PluginMenuItem(
link='plugins:netbox_topology_views:home',
link_text='Topology'
link="plugins:netbox_topology_views:images", link_text="Topology Images"
),
)
@@ -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';
+23 -3
View File
@@ -1,7 +1,27 @@
#visgraph {
height: 70vh;
}
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: 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
}
+112 -115
View File
@@ -1,31 +1,31 @@
var graph = null;
var container = null;
var downloadButton = null;
var MIME_TYPE = "image/png";
var canvas = null;
var csrftoken = null;
var nodes = new vis.DataSet();
var edges = new vis.DataSet();
var options = {
import { DataSet } from 'vis-data/esnext'
import { Network } from 'vis-network/esnext'
import { getCookie } from './csrftoken.js'
const options = {
interaction: {
hover: true,
hoverConnectedEdges: true,
multiselect: false
multiselect: true
},
nodes: {
shape: 'image',
brokenImage: '../../static/netbox_topology_views/img/role-unknown.png',
brokenImage: brokenImage ?? '',
size: 35,
font: {
multi: 'md',
face: 'helvetica',
},
color:
document.documentElement.dataset.netboxColorMode === 'dark'
? '#fff'
: '#000'
}
},
edges: {
length: 100,
width: 2,
font: {
face: 'helvetica',
face: 'helvetica'
},
shadow: {
enabled: true
@@ -34,118 +34,115 @@ var options = {
physics: {
solver: 'forceAtlas2Based'
}
};
var selected_regions = [];
var selected_sites = [];
var coord_save_checkbox = null;
var htmlElement = null;
}
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
// Load CSRF token
const csrftoken = getCookie('csrftoken')
// Render vis graph
let graph = null // vis graph instance
const container = document.querySelector('#visgraph')
const coordSaveCheckbox = document.querySelector('#id_save_coords')
;(function handleLoadData() {
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) {
container = document.createElement("div");
container.innerHTML = html;
return container;
}
const edges = new DataSet(
topologyData.edges.map((node) => ({
...node,
title: htmlTitle(node.title)
}))
)
graph = new Network(container, { nodes, edges }, options)
graph.fit()
function addEdge(item) {
item.title = htmlTitle( item.title );
edges.add(item);
}
graph.on('dragEnd', (params) => {
if (coordSaveCheckbox == null) return
if (!coordSaveCheckbox.checked) return
Promise.allSettled(
Object.entries(graph.getPositions(params.nodes)).map(
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 addNode(item) {
item.title = htmlTitle( item.title );
nodes.add(item);
}
console.log(nodeId, res.status, res.statusText)
}
)
)
})
function iniPlotboxIndex() {
document.addEventListener('DOMContentLoaded', function () {
csrftoken = getCookie('csrftoken');
container = document.getElementById('visgraph');
htmlElement = document.getElementsByTagName("html")[0];
downloadButton = document.getElementById('btnDownloadImage');
handleLoadData();
btnFullView = document.getElementById('btnFullView');
coord_save_checkbox = document.getElementById('id_save_coords');
}, false);
}
graph.on('doubleClick', (params) => {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
})
})()
// Download Graph
const MIME_TYPE = 'image/png'
const downloadButton = document.querySelector('#btnDownloadImage')
downloadButton.addEventListener('click', (e) => {
performGraphDownload()
})
function performGraphDownload() {
var tempDownloadLink = document.createElement('a');
var generatedImageUrl = canvas.toDataURL(MIME_TYPE);
const canvas = container.querySelector('canvas')
const tempDownloadLink = document.createElement('a')
const generatedImageUrl = canvas.toDataURL(MIME_TYPE)
tempDownloadLink.href = generatedImageUrl;
tempDownloadLink.download = "topology";
document.body.appendChild(tempDownloadLink);
tempDownloadLink.click();
document.body.removeChild(tempDownloadLink);
tempDownloadLink.href = generatedImageUrl
tempDownloadLink.download = 'topology'
document.body.appendChild(tempDownloadLink)
tempDownloadLink.click()
document.body.removeChild(tempDownloadLink)
}
function handleLoadData() {
if (topology_data !== null) {
if (htmlElement.dataset.netboxColorMode == "dark") {
options.nodes.font.color = "#fff";
}
// Theme switching
const observer = new MutationObserver((mutations) =>
mutations.forEach((mutation) => {
if (
!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;
nodes = new vis.DataSet();
edges = new vis.DataSet();
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
topology_data.edges.forEach(addEdge);
topology_data.nodes.forEach(addNode);
graph.fit();
canvas = document.getElementById('visgraph').getElementsByTagName('canvas')[0];
downloadButton.onclick = function(e) { 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);
}
}
}
});
}
}
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-netbox-color-mode']
})
@@ -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
View File
@@ -0,0 +1,454 @@
{
"name": "netbox_topology_views",
"version": "3.1.1",
"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.1",
"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"
}
}
+356
View File
@@ -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==
+15
View File
@@ -0,0 +1,15 @@
from extras.plugins import PluginTemplateExtension
from django.conf import settings
from packaging import version
NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
class SiteButtons(PluginTemplateExtension):
model = 'dcim.site'
def buttons(self):
return self.render('netbox_topology_views/site_button.html')
template_extensions = [SiteButtons]
@@ -0,0 +1,33 @@
{% load static %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
<div class="modal-header">
<h5 class="modal-title">Topology Views</h5>
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<i class="mdi mdi-download"></i>
Download
</a>
<a class="btn btn-sm btn-danger" data-bs-dismiss="modal">
<i class="mdi mdi-close-thick"></i>
Close
</a>
</div>
</div>
</div>
<div class="panel-body" >
<div style="width: 500px; height: 500px;">
<div id="visgraph" style="width: 100%; height: 100%;">
</div>
</div>
</div>
<script type="text/javascript">
const brokenImage = '{{ broken_image }}';
const topologyData = {{ topology_data | safe }};
</script>
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
@@ -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="panel-body">
<div id="visgraph" class=""></div>
<div id="visgraph"></div>
</div>
<script type="text/javascript">
var topology_data = {{ topology_data | safe }};
</script>
</div>
{% if filter_form %}
@@ -69,7 +65,9 @@
{% endblock content-wrapper %}
{% block javascript %}
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
<script type="application/javascript">iniPlotboxIndex()</script>
<script type="text/javascript">
const brokenImage = '{{ broken_image }}';
const topologyData = {{ topology_data | safe }};
</script>
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
{% endblock javascript %}
@@ -0,0 +1,4 @@
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
Topology
</a>
@@ -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>
+4 -2
View File
@@ -1,10 +1,12 @@
from django.urls import path
from django.views.generic.base import RedirectView
from . import views
# 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.
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"),
)
+105
View File
@@ -0,0 +1,105 @@
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Type
import sys
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"
if sys.version_info >= (3,9,0):
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / settings.PLUGINS_CONFIG[
"netbox_topology_views"
]["static_image_directory"].removeprefix("/")
else:
prefix = "/"
plugin_path = settings.PLUGINS_CONFIG["netbox_topology_views"]["static_image_directory"]
if plugin_path.startswith(prefix):
plugin_path_new = plugin_path[len(prefix):]
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / plugin_path_new
else:
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / plugin_path
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:
url_path = settings.BASE_PATH + settings.STATIC_URL
if sys.version_info >= (3,9,0):
return url.removeprefix(url_path)
else:
if url.startswith(url_path):
url_new = url[len(url_path):]
return url_new
else:
return 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__),
)
+609 -216
View File
@@ -1,299 +1,692 @@
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
from functools import reduce
from typing import DefaultDict, Dict, Optional, Union
import time
from dcim.models import Device, Cable, DeviceRole, PathEndpoint
from circuits.models import CircuitTermination, ProviderNetwork
from utilities.htmx import is_htmx
from circuits.models import Circuit, CircuitTermination
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 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):
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
is_device = isinstance(entity, Device)
query = (
{"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>"
dev_title = "<table> %s </table>" % (node_content)
def create_node(
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool
):
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["label"] = dev_name
node["title"] = dev_title
node["shape"] = "image"
if device.device_role.slug in settings.PLUGINS_CONFIG["netbox_topology_views"]["device_img"]:
node["image"] = "../../static/netbox_topology_views/img/" + device.device_role.slug + ".png"
else:
node["image"] = "../../static/netbox_topology_views/img/role-unknown.png"
if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color
node["href"] = device.get_absolute_url()
node["image"] = get_image_for_entity(device)
node["physics"] = True
if "coordinates" in device.custom_field_data:
if device.custom_field_data["coordinates"] is not None:
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["y"] = int(cords[1])
node["physics"] = False
elif save_coords:
node["physics"] = False
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
cable_a_name = "device A name unknown" if termination_a.name is None else termination_a.name
cable_b_dev_name = "device A name unknown" if termination_b.device.name is None else termination_b.device.name
cable_b_name = "cable B name unknown" if termination_b.name is None else termination_b.name
def create_edge(
edge_id: int,
termination_a: Dict,
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["id"] = edge_id
edge["from"] = termination_a.device.id
edge["to"] = termination_b.device.id
edge["from"] = termination_a["device_id"]
edge["to"] = termination_b["device_id"]
title = "Cable"
if circuit is not None:
edge["dashes"] = True
edge["title"] = "Circuit provider: " + circuit.provider.name + "<br>"
edge["title"] += "Termination between <br>"
edge["title"] += cable_b_dev_name + " [" + cable_b_name + "]<br>"
edge["title"] += cable_a_dev_name + " [" + cable_a_name + "]"
else:
edge["title"] = "Cable between <br> " + cable_a_dev_name + " [" + cable_a_name + "]<br>" + cable_b_dev_name + " [" + cable_b_name + "]"
if path is not None:
edge["title"] += "" if len(path) <= 0 else "<br>Through " + "/".join(path)
title = f"Circuit provider: {circuit['provider_name']}<br>Termination"
elif wireless is not None:
edge["dashes"] = [2, 10, 2, 10]
title = "Wireless Connection"
elif power is not None:
edge["dashes"] = [5, 5, 3, 3]
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 != "":
edge["color"] = "#" + cable.color
return edge
def get_topology_data(queryset, hide_unconnected, intermediate_dev_role_ids, end2end_connections):
nodes_devices = {}
edges = []
edge_ids = 0
cable_ids = []
circuit_ids = []
def create_circuit_termination(termination):
if isinstance(termination, CircuitTermination):
return {
"termination_name": termination.circuit.provider.name,
"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:
return None
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
enable_circuit_terminations = settings.PLUGINS_CONFIG["netbox_topology_views"]["enable_circuit_terminations"]
nodes_devices = {}
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) ) \
.select_related("termination_a_type", "termination_b_type") \
.prefetch_related("termination_a", "termination_b")
if show_circuit:
circuit_terminations = CircuitTermination.objects.filter(
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:
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:
if link.termination_type.name in ignore_cable_type:
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):
if isinstance(link.termination_a, CircuitTermination):
if enable_circuit_terminations and link.termination_a.circuit.id not in circuit_ids:
circuit = link.termination_a.circuit
circuit_ids.append(circuit.id)
cable_ids.append(link.id)
path_destination = circuit.termination_z if link.termination_a.term_side == "A" else circuit.termination_a
if path_destination is not None and path_destination.provider_network is None:
# ProviderNetwork not supported at the moment
# $path_destination.cable would be none : there is no cable between a CircuitTermination and ProviderNetwork
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
# Normal device cables
if link.termination_type.name in supported_termination_types:
complete_link = False
if link.cable_end == "A":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "B" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["B"] is not None:
complete_link = True
elif link.cable_end == "B":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "A" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["A"] is not None:
complete_link = True
else:
continue
print("Unkown cable end")
cable_ids[link.cable_id][link.cable_end] = link
path_destination = path_start.path.destination
if hasattr(path_destination, "device") and path_destination.device is not None and path_destination.device.id not in device_ids:
# device not in queryset, skip
continue
if complete_link:
edge_ids += 1
if isinstance(cable_ids[link.cable_id]["B"], CableTermination):
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
origin = None
circuit = None
tmp_path = []
edges.append(
create_edge(
edge_id=edge_ids,
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)
# TODO device qui ne s"affichent pas si hide_unconnected
for (termination_a, cable, termination_b) in path_start.trace():
if origin is None:
# New part of the link
origin = termination_a
circuit = None
tmp_path = []
for wlan_link in wlan_links:
if wlan_link.interface_a.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_a.device.pk
] = wlan_link.interface_a.device
if wlan_link.interface_b.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_b.device.pk
] = wlan_link.interface_b.device
if isinstance(termination_b, ProviderNetwork):
# ProviderNetwork not supported at the moment
# It would need to manage several kind of nodes (Device, ProviderNetwork)
# and maps javascript nodes ID with Devices IDs and ProviderNetworks IDs
# When $termination_b is a ProviderNetwork instance, $cable will be None
break
termination_a = {
"termination_name": wlan_link.interface_a.name,
"termination_device_name": wlan_link.interface_a.device.name,
"device_id": wlan_link.interface_a.device_id,
}
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):
# Ignore this path
break
edge_ids += 1
edges.append(
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:
if qs_device.id not in nodes_devices and not hide_unconnected:
nodes_devices[qs_device.id] = qs_device
if qs_device.pk not in nodes_devices and not hide_unconnected:
nodes_devices[qs_device.pk] = qs_device
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
return results
class TopologyHomeView(PermissionRequiredMixin, View):
permission_required = ("dcim.view_site", "dcim.view_device")
"""
Show the home page
"""
def get(self, request):
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
topo_data = None
if request.GET:
save_coords = False
if "save_coords" in request.GET:
if request.GET["save_coords"] == "on":
save_coords = True
hide_unconnected = False
if "hide_unconnected" in request.GET:
if request.GET["hide_unconnected"] == "on" :
if request.GET["hide_unconnected"] == "on":
hide_unconnected = True
if "intermediate_dev_role_id" in request.GET:
intermediate_dev_role_ids = list(map(int, request.GET.getlist("intermediate_dev_role_id")))
else:
intermediate_dev_role_ids = []
show_power = False
if "show_power" in request.GET:
if request.GET["show_power"] == "on":
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 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:
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:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_intermediate_dev_roles = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_intermediate_dev_roles"]
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"][
"preselected_device_roles"
]
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_intermediate_dev_role_id = DeviceRole.objects.filter(name__in=preselected_intermediate_dev_roles).values_list("id", flat=True)
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list("name", flat=True)
q_device_role_id = DeviceRole.objects.filter(
name__in=preselected_device_roles
).values_list("id", flat=True)
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list(
"name", flat=True
)
q = QueryDict(mutable=True)
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["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()
return HttpResponseRedirect(request.path + "?" + query_string)
return HttpResponseRedirect(f"{request.path}?{query_string}")
return render(request, "netbox_topology_views/index.html" , {
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data)
}
if is_htmx(request):
return render(
request,
"netbox_topology_views/htmx_topology.html",
{
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data),
"broken_image": find_image_url("role-unknown"),
"epoch": int(time.time()),
},
)
return render(
request,
"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,
},
)
-4781
View File
File diff suppressed because it is too large Load Diff
-27
View File
@@ -1,27 +0,0 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "2.0.1",
"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"
}
}
+3
View File
@@ -0,0 +1,3 @@
djangorestframework
django-filter
black
+18 -11
View File
@@ -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(
name='netbox-topology-views',
version='2.0.1',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',
license='Apache 2.0',
name="netbox-topology-views",
version="3.1.1",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/mattieserver/netbox-topology-views",
author="Mattijs Vanhaverbeke",
license="Apache 2.0",
install_requires=[],
packages=find_packages(),
include_package_data=True,
keywords=["netbox-plugin"],
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
],
project_urls={
'Source': 'https://github.com/mattieserver/netbox-topology-views',
"Source": "https://github.com/mattieserver/netbox-topology-views",
},
)