Compare commits

...
33 Commits
Author SHA1 Message Date
mattieserver 0004f0a6d3 bump version (#269) 2023-03-23 10:08:34 +01:00
MarioandMattijs Vanhaverbeke 5d20a7ad15 Closes #252,#78,#177,#265: Move options from config file to GUI (#262)
* Added some individual options

* Added general options

* Added show_single_cable_logical_conns and draw_default_layout

* Added ignore_cable_type to IndividualOptions

* Corrected permission, tiny improvements

* Add preselected_device_roles to IndividualOptions

* Add preselected_tags

* Updated documentation

* Update README.md

* Corrected queryset for preselected_tags

* Update README.md

* move supported_termination_types to inside the function to prevent migration issues

* removed general options

* added new migration script

---------

Co-authored-by: Mattijs Vanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
2023-03-23 09:57:51 +01:00
Mario 4fbc2fd8ce Merge pull request #268 from mattieserver/dreng-patch-1
Update pull_request_template.md
2023-03-19 16:39:36 +01:00
Mario 3e90502630 Update pull_request_template.md 2023-03-19 16:39:15 +01:00
Jan Krupa c4ee38c0c9 Change menu navigation icon (#266) 2023-03-10 09:29:33 +01:00
Mario b8a1af15ef Merge pull request #255 from royvegard/254_open_trace_for_logical_connection
Closes #254: Add a url to cable trace for logical connections
2023-01-29 19:16:23 +01:00
Roy Vegard OvesenandMario 7ecd29bb5d Avoid rendering double slash
Co-authored-by: Mario <github@franzbonenkamp.de>
2023-01-29 18:57:09 +01:00
Roy Vegard Ovesen b3b94f63d9 Add a url to cable trace for logical connections
Fixes #254
2023-01-29 11:52:50 +00:00
Mario c0b36ca8d3 Merge pull request #249 from dreng/248_update_docs
Fixes #248: Update README.md
2023-01-17 10:44:31 +01:00
Mario 77b194b680 Update README.md 2023-01-17 10:31:41 +01:00
mattieserver 6fafbeb7c3 bump version (#246) 2023-01-09 13:37:06 +01:00
Mario 6f2ac1684a Added show_cables=on to HTMX-GET parameters (#242) 2023-01-08 22:08:04 +01:00
Mario 1227c87d05 Merge pull request #243 from dreng/241_bigger_site_topology_window
Closes #241: Make Site Topology window bigger
2023-01-07 12:47:55 +01:00
Mario 89b04ff0c5 Merge branch 'mattieserver:master' into 241_bigger_site_topology_window 2023-01-06 23:50:07 +01:00
dreng 47079cf27f Raised max-width 2023-01-06 23:47:24 +01:00
Mario fbf223f8bf Merge pull request #239 from mattieserver/revert-230-update-git-ignore
Revert "Update git ignore"
2023-01-06 10:18:18 +01:00
Mario b74fbb828f Revert "Update git ignore" 2023-01-06 10:09:47 +01:00
mattieserver 2e34f7435a Release v3.2.0 (#234)
* bump version

* bump version
2023-01-05 13:12:32 +01:00
Mario 3805d75dba Merge pull request #230 from dreng/update-git-ignore
Update git ignore
2022-12-28 11:48:00 +01:00
Mario b178d68338 Merge branch 'mattieserver:master' into update-git-ignore 2022-12-28 11:40:45 +01:00
mario 37c900ea4a Added VS Code project settings to .gitignore 2022-12-28 11:36:33 +01:00
Marioandmattieserver 0cb0c10dbb Closes #181: Open cable on double-click (#225)
* Closes #181: Open cable on double-click

* build js

Co-authored-by: mattieserver <3049868+mattieserver@users.noreply.github.com>
2022-12-27 14:57:32 +01:00
dfb0b532b9 Closes #151: Implement interface end to end connection view (#188)
* Added end to end interface connections, made showing cables and wireless optional

* Fixed 'Hide Unconnected' and other bugs

* rename var + extra check

* Add filter selection image for readme

* Added guidance on using the selection options

* Renaming to selection_options

* Renamed filter_selection to selection_options

* Update README.md

* Bug fix

* fix parameter order

* differentiate line type for interface connections

* allow both cables and logical connections

* Change logical connection line type

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

* Hide redundant connections, configurable options

* updated readme + removed visjs setting from settings

* Update README.md

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

* updated readme for 'Show Logical Connections'

Co-authored-by: tagrusendorf <tagrusendorf@dev-netbox.tag.kbproductions.ca>
Co-authored-by: mattieserver <3049868+mattieserver@users.noreply.github.com>
Co-authored-by: Mario <github@franzbonenkamp.de>
Co-authored-by: Mattijs Vanhaverbeke <mattijs.vanhaverbeke@ebo-enterprises.com>
2022-12-27 14:49:34 +01:00
mattieserver e0bd1d4e86 Closes #160: feature save filters (#227)
* initial saved filter support

* add filter field

* fixed model

* changed margin
2022-12-26 12:07:01 +01:00
mattieserver 683a980858 Link to netbox-docker wiki for install in docker (#229)
* Link to netbox-docker wiki for install in docker

* cleanup old docker stuff
2022-12-23 16:05:30 +01:00
mattieserver 2befe94f5d menu layout: use new class from netbox 3.4 (#215)
* new layout

* used new PluginMenu class
2022-12-21 17:03:47 +01:00
Mario 2f65614fcb Merge pull request #226 from dreng/pull_request_template
Create pull_request_template.md
2022-12-20 19:54:44 +01:00
Mario 2722dfa793 Create pull_request_template.md 2022-12-20 19:38:36 +01:00
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
32 changed files with 1088 additions and 533 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.)
+24
View File
@@ -0,0 +1,24 @@
<!--
IMPORTANT:
==========
Thank you for contributing to NetBox Topology Views! Please
note that our contribution guidelines require a feature
request or bug report to be labeled "status: accepted". This
helps avoid waste time and effort on a proposed change that
we might not be able to accept. If a feature request or bug
report is labeled "status: needs owner", feel free to raise
your hand if you'd like to take work on it and we will
happily assign you to the issue.
Please prefix the title of your pull request according to
it's type. You should use "Closes #FEATURE_ID: ..." for feature
requests and "Fixes #ISSUE_ID: ..." for issues. This helps
us closing issues automatically after merging.
To sum it up:
1. Create Bug Report or Feature Request
2. Wait until the issue has been labeled "status: accepted"
3. Start working on the issue in a seperate branch
3. Create a pull request and name the title with a prefix
according to the issue ID.
-->
-93
View File
@@ -1,93 +0,0 @@
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 }}
-4
View File
@@ -1,4 +0,0 @@
FROM netboxcommunity/netbox:latest
COPY ./docker/plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
+44 -8
View File
@@ -12,6 +12,8 @@ Support to filter on name, site, tag and device role.
## Install
**_NOTE:_** For docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
The plugin is available as a Python package and can be installed with pip.
Run `pip install netbox-topology-views` in your virtual env.
@@ -44,6 +46,7 @@ python3 manage.py collectstatic --no-input
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| >= 3.4.0 | >= v3.X.X |
| >= 3.3.0 | >= v3.0.0 |
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
@@ -64,14 +67,17 @@ You might also set the `always_save_coordinates` option to True.
## Configure
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
All individual options can be assigned a default value per user directly in the plugin. The default value can be overridden on the filter page.
The remaining options must be configured in the `PLUGINS_CONFIG` section of your `netbox/configuration.py`.
Example:
```
PLUGINS_CONFIG = {
'netbox_topology_views': {
'preselected_device_roles': ['Router', 'Firewall'],
'enable_circuit_terminations': True
'static_image_directory': 'netbox_topology_views/img',
'allow_coordinates_saving': True,
'always_save_coordinates': True
}
}
```
@@ -79,12 +85,8 @@ PLUGINS_CONFIG = {
| 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
@@ -94,6 +96,31 @@ To change image with associated device use the `Images` page - it allows to map
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies
Select your options for the topology view:
![selection_options](https://user-images.githubusercontent.com/20901110/220410144-5633d24c-6383-4cba-8d6d-ba77ef3d3cdd.png)
<dl>
<dt>Save Coordinates</dt>
<dd>Save the coordinates of devices in the topology view.</dd>
<dt>Show Unconnected</dt>
<dd>Show devices that have no connections or for which no connection is displayed. This option depends on other parameters like 'Show Cables' and 'Show Logical Connections'.</dd>
<dt>Show Circuit Terminations</dt>
<dd>Show connections which end at a circuit termination in the topology view. These connections are displayed as blue dashed lines.</dd>
<dt>Show Cables</dt>
<dd>Show connections between interfaces, front / rear ports, etc., that are connected with one or more cables. These connections are displayed as solid lines in the color of the cable.</dd>
<dt>Show Logical Connections</dt>
<dd>Show logical connections between interfaces (referred to as Interface Connections in NetBox) in the topology view. Where the path between
interfaces includes multiple cables (e.g., via patch panels), only the end interface connections are shown, not the
intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions. These connections are displayed as yellow dotted lines.</dd>
<dt>Show redundant Cable and Locigal Connection</dt>
<dd>Shows a logical connection (in addition to a cable), even if a cable is directly connected. Leaving this option disabled prevents that redundant display. This option only has an effect if 'Show Logical Connections' is activated.</dd>
<dt>Show Power Feeds</dt>
<dd>Displays connections between power outlets and power ports. These connections are displayed as solid lines in the color of the cable. This option depends on 'Show Cables'.</dd>
<dt>Show Wireless Links</dt>
<dd>Displays wireless connections. These connections are displayed as blue dotted lines.</dd>
</dl>
### Update
Run `pip install netbox-topology-views --upgrade` in your venv.
@@ -108,12 +135,21 @@ Clear you browser cache.
### Permissions
To view `/plugins/topology-views/` you need the following permissions:
To view `/plugins/netbox_topology-views/topology` you need the following permissions:
+ dcim | device | can view device
+ dcim | site | can view site
+ extras | tag | can view tag
+ dcim | device role | can view device role
To view `/plugins/netbox_topology-views/image`:
+ dcim | site | view
+ dcim | device role | view
+ dcim | device role | add
+ dcim | device role | change
To view `/plugins/netbox_topology-views/individualoptions`:
+ netbox topology views | individualoptions | change
## Icons info
Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power).
+138 -18
View File
@@ -1,23 +1,143 @@
# Setup Dev
+ install docker
+ install docker compose
+ install npm
+ enable yarn
+ 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 `yarn run bundle`
# 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

-1
View File
@@ -1 +0,0 @@
netbox-topology-views
+2 -16
View File
@@ -5,30 +5,16 @@ class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "3.1.0"
version = "3.3.0"
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",
],
"ignore_cable_type": [],
"static_image_directory": "netbox_topology_views/img",
"allow_coordinates_saving": False,
"always_save_coordinates": False,
"preselected_tags": [],
"draw_default_layout": False,
}
+7 -1
View File
@@ -1,7 +1,8 @@
from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from netbox_topology_views.models import RoleImage
from netbox_topology_views.models import RoleImage, IndividualOptions
class TopologyDummySerializer(ModelSerializer):
@@ -20,3 +21,8 @@ class DeviceRoleSerializer(ModelSerializer):
class Meta:
model = DeviceRole
fields = ("name", "slug", "color", "vm_role", "description")
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
fields = ("ignore_cable_type", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_circuit", "show_power", "show_wireless", "draw_default_layout")
+147 -15
View File
@@ -12,17 +12,14 @@ from dcim.choices import DeviceStatusChoices
from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms import (
TagFilterField,
DynamicModelMultipleChoiceField,
MultipleChoiceField,
widgets,
)
allow_coordinates_saving = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
)
from .models import IndividualOptions
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device
@@ -31,10 +28,15 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
None,
(
"q",
"hide_unconnected",
"filter_id",
"save_coords",
"show_unconnected",
"show_cables",
"show_circuit",
"show_logical_connections",
"show_single_cable_logical_conns",
"show_power",
"show_wireless",
),
),
(
@@ -100,8 +102,32 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
},
label=_("Rack"),
)
hide_unconnected = forms.BooleanField(
label=_("Hide Unconnected"), required=False, initial=False
status = MultipleChoiceField(
choices=DeviceStatusChoices, required=False, label=_("Device Status")
)
tag = TagFilterField(model)
# options
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
required=False,
disabled=(not settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] or settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]),
initial=(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"])
)
show_unconnected = forms.BooleanField(
label=_("Show Unconnected"), required=False, initial=False
)
show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"), required=False, initial=False
)
show_single_cable_logical_conns = forms.BooleanField(
label =_("Show redundant Cable and Locigal Connection"), required=False, initial=False
)
show_cables = forms.BooleanField(
label =_("Show Cables"), required=False, initial=False
)
show_wireless = forms.BooleanField(
label =_("Show Wireless Links"), required=False, initial=False
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"), required=False, initial=False
@@ -109,12 +135,118 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
show_power = forms.BooleanField(
label=_("Show Power Feeds"), required=False, initial=False
)
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
class IndividualOptionsForm(NetBoxModelForm):
fieldsets = (
(
None,
(
"user_id",
"ignore_cable_type",
"preselected_device_roles",
"preselected_tags",
"show_unconnected",
"show_cables",
"show_circuit",
"show_logical_connections",
"show_single_cable_logical_conns",
"show_power",
"show_wireless",
"draw_default_layout",
),
),
)
user_id = forms.CharField(widget=forms.HiddenInput())
ignore_cable_type = MultipleChoiceField(
label=_("Ignore Termination Types"),
required=False,
choices=IndividualOptions.CHOICES,
help_text=_("Choose Termination Types that you want to be ignored. "
"If any ignored Termination Type is part of a connection, the "
"cable is not displayed.")
)
preselected_device_roles = DynamicModelMultipleChoiceField(
label=_("Preselected Device Role"),
queryset=DeviceRole.objects.all(),
required=False,
disabled=(not allow_coordinates_saving),
help_text=_("Select Device Roles that you want to have "
"preselected in the filter tab.")
)
status = MultipleChoiceField(
choices=DeviceStatusChoices, required=False, label=_("Device Status")
preselected_tags = forms.ModelMultipleChoiceField(
label=_("Preselected Tags"),
queryset=Device.tags.all(),
required=False,
widget=widgets.StaticSelectMultiple,
help_text=_("Select Tags that you want to have "
"preselected in the filter tab.")
)
tag = TagFilterField(model)
show_unconnected = forms.BooleanField(
label=_("Show Unconnected"),
required=False,
initial=False,
help_text=_("Draws devices that have no connections or for which no "
"connection is displayed. This option depends on other parameters "
"like 'Show Cables' and 'Show Logical Connections'.")
)
show_cables = forms.BooleanField(
label =_("Show Cables"),
required=False,
initial=False,
help_text=_("Displays connections between interfaces that are connected "
"with one or more cables. These connections are displayed as solid "
"lines in the color of the cable.")
)
show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"),
required=False,
initial=False,
help_text=_("Displays connections between devices that are not "
"directly connected (e.g. via patch panels). These connections "
"are displayed as yellow dotted lines.")
)
show_single_cable_logical_conns = forms.BooleanField(
label = ("Show redundant Cable and Locigal Connection"),
required = False,
initial=False,
help_text=_("Shows a logical connection (in addition to a cable), "
"even if a cable is directly connected. Leaving this option "
"disabled prevents that redundant display. This option only "
"has an effect if 'Show Logical Connections' is activated.")
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"),
required=False,
initial=False,
help_text=_("Displays connections between circuit terminations. "
"These connections are displayed as blue dashed lines.")
)
show_power = forms.BooleanField(
label=_("Show Power Feeds"),
required=False,
initial=False,
help_text=_("Displays connections between power outlets and power "
"ports. These connections are displayed as solid lines in the "
"color of the cable. This option depends on 'Show Cables'.")
)
show_wireless = forms.BooleanField(
label =_("Show Wireless Links"),
required=False,
initial=False,
help_text=_("Displays wireless connections. These connections are "
"displayed as blue dotted lines.")
)
draw_default_layout = forms.BooleanField(
label = ("Draw Default Layout"),
required=False,
initial=False,
help_text=_("Enable this option if you want to draw the topology on "
"the initial load (when you go to the topology plugin page).")
)
class Meta:
model = IndividualOptions
fields = [
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_circuit', 'show_power', 'show_wireless', 'draw_default_layout'
]
@@ -0,0 +1,42 @@
# Generated by Django 4.1.4 on 2023-03-19 14:13
from django.db import migrations, models
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('dcim', '0167_module_status'),
('extras', '0084_staging'),
('netbox_topology_views', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='IndividualOptions',
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)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('user_id', models.IntegerField(null=True, unique=True)),
('ignore_cable_type', models.CharField(blank=True, max_length=255)),
('show_unconnected', models.BooleanField(default=False)),
('show_cables', models.BooleanField(default=False)),
('show_logical_connections', models.BooleanField(default=False)),
('show_single_cable_logical_conns', models.BooleanField(default=False)),
('show_circuit', models.BooleanField(default=False)),
('show_power', models.BooleanField(default=False)),
('show_wireless', models.BooleanField(default=False)),
('draw_default_layout', models.BooleanField(default=False)),
('preselected_device_roles', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_device', related_name='+', to='dcim.devicerole')),
('preselected_tags', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_tag', related_name='+', to='extras.tag')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'abstract': False,
},
),
]
+61
View File
@@ -2,10 +2,12 @@ from pathlib import Path
from typing import Optional
from dcim.models import DeviceRole
from extras.models import Tag
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 import NetBoxModel
from netbox.models.features import (
ChangeLoggingMixin,
ExportTemplatesMixin,
@@ -91,3 +93,62 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
except ValueError:
return self.get_default_image(dir)
return static(f"/{self.image}")
class IndividualOptions(NetBoxModel):
CHOICES = (
('interface', 'interface'),
('front port', 'front port'),
('rear port', 'rear port'),
('power outlet', 'power outlet'),
('power port', 'power port'),
('console port', 'console port'),
('console server port', 'console server port'),
)
user_id = models.IntegerField(
null=True,
unique=True
)
ignore_cable_type = models.CharField(
max_length = 255,
blank = True,
)
preselected_device_roles = models.ManyToManyField(
to='dcim.DeviceRole',
related_name='+',
blank=True,
db_table='netbox_topology_views_individualoptions_preselected_device',
)
preselected_tags = models.ManyToManyField(
to='extras.Tag',
related_name='+',
blank=True,
db_table='netbox_topology_views_individualoptions_preselected_tag',
)
show_unconnected = models.BooleanField(
default=False
)
show_cables = models.BooleanField(
default=False
)
show_logical_connections = models.BooleanField(
default=False
)
show_single_cable_logical_conns = models.BooleanField(
default=False
)
show_circuit = models.BooleanField(
default=False
)
show_power = models.BooleanField(
default=False
)
show_wireless = models.BooleanField(
default=False
)
draw_default_layout = models.BooleanField(
default=False
)
def __str___(self):
return f"{self.user_id}"
+12 -5
View File
@@ -1,8 +1,15 @@
from extras.plugins import PluginMenuItem
from extras.plugins import PluginMenu, PluginMenuItem
menu_items = (
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
PluginMenuItem(
link="plugins:netbox_topology_views:images", link_text="Topology Images"
menu = PluginMenu(
label='Topology Views',
icon_class="mdi mdi-sitemap",
groups=(
('TOPOLOGY', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
('PREFERENCES',
(
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"),
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options"),
),
),
),
)
@@ -1 +1 @@
#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}
#visgraph{height:64vh}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
@@ -1,5 +1,5 @@
#visgraph {
height: 70vh;
height: 64vh;
}
html[data-netbox-color-mode=dark] #visgraph {
+12 -4
View File
@@ -70,8 +70,9 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
graph.fit()
graph.on('dragEnd', (params) => {
if (coordSaveCheckbox == null) return
if (!coordSaveCheckbox.checked) return
Promise.allSettled(
Object.entries(graph.getPositions(params.nodes)).map(
async ([nodeId, nodePosition]) => {
@@ -99,9 +100,16 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
})
graph.on('doubleClick', (params) => {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
if (params.nodes.length > 0) {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
}
else {
params.edges.forEach((edge) => {
window.open(edges.get(edge).href, '_blank')
})
}
})
})()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "netbox_topology_views",
"version": "3.1.0",
"version": "3.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "3.1.0",
"version": "3.3.0",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
+72 -177
View File
@@ -4,44 +4,39 @@
"@egjs/hammerjs@^2.0.0":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
resolved "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz"
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"
resolved "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz"
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==
version "3.1.3"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
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"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
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"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
@@ -56,92 +51,17 @@ braces@~3.0.2:
component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz"
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-linux-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
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"
resolved "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz"
integrity sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==
dependencies:
esbuild "^0.14.5"
@@ -149,155 +69,130 @@ esbuild-sass-plugin@^1.5.2:
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"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz"
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==
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz"
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
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"
"@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"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
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"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
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"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
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"
resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
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"
resolved "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz"
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"
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
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==
version "2.11.0"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz"
integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
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"
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
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"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
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"
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
keycharm@^0.4.0:
"keycharm@^0.2.0 || ^0.3.0 || ^0.4.0", keycharm@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/keycharm/-/keycharm-0.4.0.tgz#8d684ea9cc01379a07fbddee33ff32d97f5ae2a7"
resolved "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz"
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"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
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"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
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"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
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"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
is-core-module "^2.9.0"
@@ -305,9 +200,9 @@ resolve@^1.20.0:
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==
version "1.56.2"
resolved "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz"
integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@@ -315,42 +210,42 @@ sass@^1.45.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"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
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"
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz"
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"
resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
uuid@^8.0.0:
"uuid@^3.4.0 || ^7.0.0 || ^8.0.0", "uuid@^7.0.0 || ^8.0.0", uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
vis-data@^7.1.4:
vis-data@^7.0.0, vis-data@^7.1.4:
version "7.1.4"
resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.4.tgz#90e5e796a79e1901de14c0808fb32a1a0735c1dc"
resolved "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz"
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"
resolved "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz"
integrity sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==
vis-util@^5.0.3:
"vis-util@^4.0.0 || ^5.0.0", vis-util@^5.0.1, vis-util@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vis-util/-/vis-util-5.0.3.tgz#6a9d67c6ed92f2bfa67e50c20166bb5f59ac07bf"
resolved "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz"
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,39 @@
{% 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="max-width: 900px; height: 500px;">
<div id="visgraph" style="width: 100%; height: 100%;">
</div>
</div>
</div>
<style type="text/css">
.modal-dialog {
max-width: 902px !important;
}
</style>
<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>
@@ -48,6 +48,12 @@
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content">
{# Applied filters #}
{% if filter_form %}
{% applied_filters model filter_form request.GET %}
<hr style="margin-top: 0px;">
{% endif %}
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
<div class="panel-body">
<div id="visgraph"></div>
@@ -0,0 +1,5 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Topology Views Individual Options{% endblock %}
{% block tabs %} {% endblock tabs %}
@@ -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 }}&show_cables=on" 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>
+1
View File
@@ -9,4 +9,5 @@ urlpatterns = (
path("", RedirectView.as_view(url="topology/", permanent=True)),
path("topology/", views.TopologyHomeView.as_view(), name="home"),
path("images/", views.TopologyImagesView.as_view(), name="images"),
path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"),
)
+23 -4
View File
@@ -3,15 +3,26 @@ 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"
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / settings.PLUGINS_CONFIG[
"netbox_topology_views"
]["static_image_directory"].removeprefix("/")
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:
@@ -21,7 +32,15 @@ def image_static_url(path: Path) -> str:
def get_image_from_url(url: str) -> str:
return url.removeprefix(settings.BASE_PATH + settings.STATIC_URL)
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 = (
+308 -176
View File
@@ -1,12 +1,15 @@
import json
from functools import reduce
from typing import DefaultDict, Dict, Optional, Union
import time
from utilities.htmx import is_htmx
from circuits.models import Circuit, CircuitTermination
from dcim.models import (
Cable,
CableTermination,
Device,
device_components,
DeviceRole,
FrontPort,
Interface,
@@ -15,9 +18,11 @@ from dcim.models import (
RearPort,
)
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, QuerySet
from django.db.models.functions import Lower
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
from django.shortcuts import render
from django.views.generic import View
@@ -25,8 +30,8 @@ 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.forms import DeviceFilterForm, IndividualOptionsForm
from netbox_topology_views.models import RoleImage, IndividualOptions
from netbox_topology_views.utils import (
CONF_IMAGE_DIR,
find_image_url,
@@ -35,15 +40,6 @@ from netbox_topology_views.utils import (
image_static_url,
)
supported_termination_types = [
"interface",
"front port",
"rear port",
"power outlet",
"power port",
"console port",
"console server port",
]
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
@@ -175,6 +171,7 @@ def create_edge(
cable: Optional[Cable] = None,
wireless: Optional[Dict] = None,
power: Optional[bool] = None,
interface: Optional[Interface] = None,
):
cable_a_name = (
"device A name unknown"
@@ -215,12 +212,22 @@ def create_edge(
edge["dashes"] = [5, 5, 3, 3]
title = "Power Connection"
elif interface is not None:
title = "Interface Connection"
edge["width"] = 3
edge["dashes"] = [1, 10, 1, 10]
edge["arrows"] = {"to": {"enabled": True, "scaleFactor": 0.5}, "from": {"enabled": True, "scaleFactor": 0.5}}
edge["color"] = '#f1c232'
edge["href"] = interface.get_absolute_url() + "trace"
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
if cable is not None:
edge["href"] = cable.get_absolute_url()
if hasattr(cable, 'color') and cable.color != "":
edge["color"] = "#" + cable.color
return edge
@@ -247,11 +254,21 @@ def create_circuit_termination(termination):
def get_topology_data(
queryset: QuerySet,
hide_unconnected: bool,
individualOptions: IndividualOptions,
show_unconnected: bool,
save_coords: bool,
show_cables: bool,
show_circuit: bool,
show_logical_connections: bool,
show_single_cable_logical_conns: bool,
show_power: bool,
show_wireless: bool,
):
supported_termination_types = []
for t in IndividualOptions.CHOICES:
supported_termination_types.append(t[1])
if not queryset:
return None
@@ -264,9 +281,9 @@ def get_topology_data(
nodes_powerfeed: Dict[int, PowerFeed] = {}
nodes_provider_networks = {}
cable_ids = DefaultDict(dict)
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
"ignore_cable_type"
]
interface_ids = DefaultDict(dict)
ignore_cable_type = individualOptions.ignore_cable_type
device_ids = [d.pk for d in queryset]
site_ids = [d.site_id for d in queryset]
@@ -278,7 +295,7 @@ def get_topology_data(
for circuit_termination in circuit_terminations:
circuit_termination: CircuitTermination
if (
not hide_unconnected
show_unconnected
and circuit_termination.circuit_id not in nodes_circuits
):
nodes_circuits[
@@ -312,6 +329,7 @@ def get_topology_data(
edges.append(
create_edge(
edge_id=edge_ids,
cable=circuit_termination.cable,
circuit=circuit_model,
termination_a=termination_a,
termination_b=termination_b,
@@ -334,7 +352,7 @@ def get_topology_data(
if termination.device_id in device_ids:
circuit_has_connections = True
if circuit_has_connections and hide_unconnected:
if circuit_has_connections and not show_unconnected:
if circuit_termination.circuit_id not in nodes_circuits:
nodes_circuits[
circuit_termination.circuit.pk
@@ -343,15 +361,6 @@ def get_topology_data(
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)
@@ -361,15 +370,15 @@ def get_topology_data(
)
for power_feed in power_feeds:
if not hide_unconnected or (
hide_unconnected and power_feed.cable_id is not None
if show_unconnected or (
not show_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 not show_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
@@ -405,114 +414,156 @@ def get_topology_data(
for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords))
for link in links:
if link.termination_type.name in ignore_cable_type:
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:
print("Unkown cable end")
cable_ids[link.cable_id][link.cable_end] = link
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"]
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"]
edges.append(
create_edge(
edge_id=edge_ids,
cable=link.cable,
termination_a=termination_a,
termination_b=termination_b,
)
)
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
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}
edge_ids += 1
edges.append(
create_edge(
edge_id=edge_ids,
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
)
if show_logical_connections:
interfaces = Interface.objects.filter(
Q(_path__is_complete=True) & Q(device_id__in=device_ids)
)
for interface in interfaces:
# print('{} {} {} {}'.format(interface.device.name, interface.name, interface._path.destinations[0].device.name, interface._path.destinations[0].name))
for destination in interface._path.destinations:
if isinstance(destination, device_components.Interface):
if destination.device.id not in device_ids:
# print('Destination interface not in device queryset, ignoring')
continue
if destination.id in interface_ids:
# we've already captured the destination interface, ignore this connection
# print('Destination interface already exists, ignoring')
continue
if not show_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables:
# interface connection is the same as the cable connection, ignore this connection
continue
interface_ids[interface.id]=interface
edge_ids += 1
termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id }
termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id }
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=interface))
nodes_devices[interface.device.id] = interface.device
nodes_devices[destination.device.id] = destination.device
if show_cables:
links: QuerySet[CableTermination] = CableTermination.objects.filter(
Q(_device_id__in=device_ids)
).select_related("termination_type")
for link in links:
if link.termination_type.name in ignore_cable_type:
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:
print("Unkown cable end")
cable_ids[link.cable_id][link.cable_end] = link
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"]
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"]
edges.append(
create_edge(
edge_id=edge_ids,
cable=link.cable,
termination_a=termination_a,
termination_b=termination_b,
)
)
if show_wireless:
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
Q(_interface_a_device_id__in=device_ids)
& Q(_interface_b_device_id__in=device_ids)
)
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
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}
edge_ids += 1
edges.append(
create_edge(
edge_id=edge_ids,
cable=wlan_link,
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
)
)
for qs_device in queryset:
if qs_device.pk not in nodes_devices and not hide_unconnected:
if qs_device.pk not in nodes_devices and show_unconnected:
nodes_devices[qs_device.pk] = qs_device
results = {}
@@ -538,18 +589,30 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"device_type", "device_role"
)
self.queryset = self.filterset(request.GET, self.queryset).qs
self.model = self.queryset.model
topo_data = None
individualOptions, created = IndividualOptions.objects.get_or_create(
user_id=request.user.id,
)
if request.GET:
save_coords = False
if "save_coords" in request.GET:
if request.GET["save_coords"] == "on":
save_coords = True
# General options overrides
if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False:
save_coords = False
messages.warning(request, "Coordinate saving not allowed. Setting has been overridden")
elif settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"] == True:
save_coords = True
hide_unconnected = False
if "hide_unconnected" in request.GET:
if request.GET["hide_unconnected"] == "on":
hide_unconnected = True
# Individual options
show_unconnected = False
if "show_unconnected" in request.GET:
if request.GET["show_unconnected"] == "on":
show_unconnected = True
show_power = False
if "show_power" in request.GET:
@@ -561,54 +624,77 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET["show_circuit"] == "on":
show_circuit = True
if "draw_init" in request.GET:
if request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(
self.queryset,
hide_unconnected,
save_coords,
show_circuit,
show_power,
)
else:
show_logical_connections = False
if "show_logical_connections" in request.GET:
if request.GET["show_logical_connections"] == "on" :
show_logical_connections = True
show_single_cable_logical_conns = False
if "show_single_cable_logical_conns" in request.GET:
if request.GET["show_single_cable_logical_conns"] == "on" :
show_single_cable_logical_conns = True
show_cables = False
if "show_cables" in request.GET:
if request.GET["show_cables"] == "on" :
show_cables = True
show_wireless = False
if "show_wireless" in request.GET:
if request.GET["show_wireless"] == "on" :
show_wireless = True
if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(
self.queryset,
hide_unconnected,
individualOptions,
show_unconnected,
save_coords,
show_cables,
show_circuit,
show_logical_connections,
show_single_cable_logical_conns,
show_power,
show_wireless,
)
else:
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_tags = Tag.objects.filter(name__in=preselected_tags).values_list(
"name", flat=True
)
# No GET-Request in URL. We most likely came here from the navigation menu.
preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True)
preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True)
q = QueryDict(mutable=True)
q.setlist("device_role_id", list(q_device_role_id))
q.setlist("tag", list(q_tags))
q["draw_init"] = settings.PLUGINS_CONFIG["netbox_topology_views"][
"draw_default_layout"
]
if always_save_coordinates:
q["save_coords"] = "on"
q.setlist("device_role_id", list(preselected_device_roles))
q.setlist("tag", list(preselected_tags))
if individualOptions.show_unconnected: q['show_unconnected'] = "on"
if individualOptions.show_cables: q['show_cables'] = "on"
if individualOptions.show_logical_connections: q['show_logical_connections'] = "on"
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on"
if individualOptions.show_circuit: q['show_circuit'] = "on"
if individualOptions.show_power: q['show_power'] = "on"
if individualOptions.show_wireless: q['show_wireless'] = "on"
if individualOptions.draw_default_layout:
q['draw_init'] = "true"
else:
q['draw_init'] = "false"
query_string = q.urlencode()
return HttpResponseRedirect(f"{request.path}?{query_string}")
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",
@@ -616,6 +702,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data),
"broken_image": find_image_url("role-unknown"),
"model": self.model,
},
)
@@ -676,3 +763,48 @@ class TopologyImagesView(PermissionRequiredMixin, View):
"images": images,
},
)
class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
permission_required = 'netbox_topology_views.change_individualoptions'
def post(self, request):
instance = IndividualOptions.objects.get(user_id=request.user.id)
form = IndividualOptionsForm(request.POST, instance=instance)
if form.is_valid():
form.save()
messages.success(request, "Options have been sucessfully saved")
else:
messages.error(request, form.errors)
return HttpResponseRedirect("./")
def get(self, request):
queryset, created = IndividualOptions.objects.get_or_create(
user_id=request.user.id,
)
form = IndividualOptionsForm(
initial={
'user_id': request.user.id,
'ignore_cable_type': tuple(queryset.ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')),
'preselected_device_roles': IndividualOptions.objects.get(id=queryset.id).preselected_device_roles.all(),
'preselected_tags': IndividualOptions.objects.get(id=queryset.id).preselected_tags.all(),
'show_unconnected': queryset.show_unconnected,
'show_cables': queryset.show_cables,
'show_logical_connections': queryset.show_logical_connections,
'show_single_cable_logical_conns': queryset.show_single_cable_logical_conns,
'show_circuit': queryset.show_circuit,
'show_power': queryset.show_power,
'show_wireless': queryset.show_wireless,
'draw_default_layout': queryset.draw_default_layout,
},
)
return render(
request,
"netbox_topology_views/individual_options.html",
{
"form": form,
"object": queryset,
},
)
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup(
name="netbox-topology-views",
version="3.1.0",
version="3.3.0",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",