Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fafbeb7c3 | ||
|
|
6f2ac1684a | ||
|
|
1227c87d05 | ||
|
|
89b04ff0c5 | ||
|
|
47079cf27f | ||
|
|
fbf223f8bf | ||
|
|
b74fbb828f | ||
|
|
2e34f7435a | ||
|
|
3805d75dba | ||
|
|
b178d68338 | ||
|
|
37c900ea4a | ||
|
|
0cb0c10dbb | ||
|
|
dfb0b532b9 | ||
|
|
e0bd1d4e86 | ||
|
|
683a980858 | ||
|
|
2befe94f5d | ||
|
|
2f65614fcb | ||
|
|
2722dfa793 | ||
|
|
41d6afc336 | ||
|
|
2901dbb5c7 | ||
|
|
37122cd76c | ||
|
|
7173564ea9 | ||
|
|
be402b1b5c | ||
|
|
f187e75fff | ||
|
|
c6df846bde | ||
|
|
52c2611e9d | ||
|
|
5c87694b87 | ||
|
|
94f5314649 | ||
|
|
32042ff2cc | ||
|
|
9ce13cd1d2 | ||
|
|
b4e2f2418e | ||
|
|
5563e3c084 | ||
|
|
77a8a2c0d6 | ||
|
|
010179742c | ||
|
|
f2bbaed317 | ||
|
|
8c56f21f16 | ||
|
|
eb627bdedc | ||
|
|
291c033e34 | ||
|
|
1f389b3faa | ||
|
|
3d3ddaa7c4 | ||
|
|
4ea2097429 | ||
|
|
cfaba462d5 | ||
|
|
5e2f1cca57 | ||
|
|
b2e2a4c781 | ||
|
|
318b8495b7 | ||
|
|
5e1d5d2937 | ||
|
|
0b6df3bd28 | ||
|
|
af1666b3ba | ||
|
|
31a8c756e2 | ||
|
|
ce39c98396 | ||
|
|
faf156a6b2 | ||
|
|
1b36b9e7df | ||
|
|
652a44437e | ||
|
|
76141e70ce | ||
|
|
76b4f6ac9a | ||
|
|
953d7480af | ||
|
|
f323278f00 | ||
|
|
56a6f22bf6 | ||
|
|
b9d3cc3c4e | ||
|
|
d4833071df | ||
|
|
877d308574 | ||
|
|
83b662eac3 | ||
|
|
53a996edbc | ||
|
|
398deb5382 | ||
|
|
b5991a5f50 | ||
|
|
de92627176 | ||
|
|
6be028b9dc | ||
|
|
8997c38263 | ||
|
|
d927e40867 | ||
|
|
62c0dbddb2 | ||
|
|
e68a8a1af0 | ||
|
|
68e8aa0512 | ||
|
|
b792128415 | ||
|
|
f5d9f589bf | ||
|
|
3e5b28f208 | ||
|
|
3b4209223e | ||
|
|
6fc7cce79a | ||
|
|
fd4a1cd0c7 | ||
|
|
2deaed1adc | ||
|
|
55a61d4d10 | ||
|
|
3a4652543f | ||
|
|
9b4dd77943 | ||
|
|
91a690f1c8 | ||
|
|
5f498f9ce2 | ||
|
|
195ac47542 | ||
|
|
a25be917ee | ||
|
|
079361b190 | ||
|
|
a314d2b307 | ||
|
|
0340522f59 | ||
|
|
2edd597871 | ||
|
|
5f4f97c360 | ||
|
|
17ae8b4a1d | ||
|
|
bb9765e011 | ||
|
|
5bfc47775b | ||
|
|
216508170f | ||
|
|
0faa135978 | ||
|
|
9ba61cce28 | ||
|
|
9824a3f659 | ||
|
|
4115584306 | ||
|
|
cd239ac6ae | ||
|
|
030844f2fb | ||
|
|
2e6dd7de23 | ||
|
|
de5707ff06 | ||
|
|
371c5c3502 | ||
|
|
26139cbb1c | ||
|
|
4ea315d4b2 | ||
|
|
40ae715eb5 | ||
|
|
204098a143 | ||
|
|
9b606643ea | ||
|
|
f184bdf2f3 | ||
|
|
370eff6b67 | ||
|
|
6d28ce2da4 | ||
|
|
8a97923bb2 | ||
|
|
7f0dcd97f7 | ||
|
|
0d7052d83d | ||
|
|
e50945f8b5 | ||
|
|
987aab772f | ||
|
|
2ef6e7cb96 | ||
|
|
570d241b7f | ||
|
|
2025bba6a1 | ||
|
|
22ec4fb310 | ||
|
|
85b27895e2 | ||
|
|
940b37428c | ||
|
|
eedcf65bd8 | ||
|
|
bf28d2d2d1 |
@@ -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
|
||||
@@ -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.)
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
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.
|
||||
|
||||
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.
|
||||
-->
|
||||
@@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '16 22 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
@@ -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
@@ -128,4 +128,5 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
node_modules
|
||||
node_modules
|
||||
app.js.map
|
||||
@@ -1,27 +1,69 @@
|
||||
# Netbox Topology Views Plugin
|
||||
|
||||
 
|
||||
|
||||
Create topology views/maps from your devices in netbox.
|
||||
Support to filter on name, site and device role.
|
||||
The connections are based on the cables you created in netbox.
|
||||
Support to filter on name, site, tag and device role.
|
||||
|
||||
## Preview
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
Add 'netbox_topology_views' to the PLUGINS array in your django configuration file.
|
||||
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
|
||||
|
||||
Then run `python3 manage.py collectstatic --no-input`
|
||||
```no-highlight
|
||||
# echo netbox-topology-views >> local_requirements.txt
|
||||
```
|
||||
|
||||
Once installed, the plugin needs to be enabled in your `configuration.py`
|
||||
|
||||
```python
|
||||
# In your configuration.py
|
||||
PLUGINS = ["netbox_topology_views"]
|
||||
```
|
||||
|
||||
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.4.0 | >= v3.X.X |
|
||||
| >= 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 fiels.
|
||||
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
|
||||
|
||||
@@ -31,27 +73,83 @@ 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|
|
||||
| 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 |
|
||||
| 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) |
|
||||
| hide_single_cable_logical_conns | False | (bool) Set to True if you want to hide duplicate cables & logical connections |
|
||||
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
Go to the plugins tab in the navbar and click topology or go to NETBOX_URL/plugins/topology-views/ view your topologies
|
||||
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:
|
||||
|
||||

|
||||
|
||||
<dl>
|
||||
<dt>Hide Unconnected</dt>
|
||||
<dd>Hide devices which have no connections.</dd>
|
||||
<dt>Save Coordinates</dt>
|
||||
<dd>Save the coordinates of devices in the topology view.</dd>
|
||||
<dd>Please read the "Configure" chapter to set the allow_coordinates_saving option to True.</dd>
|
||||
<dt>Show Cables</dt>
|
||||
<dd>Show cable connections between devices, including cables connected to interfaces, front / rear ports, etc., in the topology view</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. </dd>
|
||||
<dd><i>Selecting both 'Show Cables' and 'Show Logical Interfaces' will sometimes result in a topology view with 2 cables per connection as these devices are diretly connected. Select only one of the options or set `hide_single_cable_logical_conns` to True.</i></dd>
|
||||
<dt>Show Circuit Terminations</dt>
|
||||
<dd>Show connections which end at a circuit termination in the topology view.</dd>
|
||||
<dt>Show Wireless Links</dt>
|
||||
<dd>Show wireless connections in the topology view.</dd>
|
||||
<dt>Show Power Feeds</dt>
|
||||
<dd>Show power connections from power feeds in the topology view.</dd>
|
||||
</dl>
|
||||
|
||||
### Update
|
||||
|
||||
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`
|
||||
|
||||
|
||||
Clear you browser cache.
|
||||
|
||||
|
||||
### Permissions
|
||||
|
||||
To view `/plugins/topology-views/` 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
|
||||
|
||||
## 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
@@ -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.
|
||||
@@ -0,0 +1,143 @@
|
||||
# Setup Dev
|
||||
|
||||
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 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.
|
||||
@@ -0,0 +1,40 @@
|
||||
###################################################################
|
||||
# This file serves as a base configuration for testing purposes #
|
||||
# only. It is not intended for production use. #
|
||||
###################################################################
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
DATABASE = {
|
||||
'NAME': 'netbox',
|
||||
'USER': 'netbox',
|
||||
'PASSWORD': 'J5brHrAXFLQSif0K',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '',
|
||||
'CONN_MAX_AGE': 300,
|
||||
}
|
||||
|
||||
DEBUG = True
|
||||
|
||||
PLUGINS = [
|
||||
'netbox_topology_views',
|
||||
]
|
||||
|
||||
REDIS = {
|
||||
'tasks': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6379,
|
||||
'PASSWORD': 't4Ph722qJ5QHeQ1qfu36',
|
||||
'DATABASE': 0,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
'HOST': 'localhost',
|
||||
'PORT': 6378,
|
||||
'PASSWORD': 'H733Kdjndks81',
|
||||
'DATABASE': 1,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
|
||||
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
@@ -0,0 +1,41 @@
|
||||
version: '3.4'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12-alpine
|
||||
environment:
|
||||
POSTGRES_PASSWORD: J5brHrAXFLQSif0K #just for local dev
|
||||
POSTGRES_DB: netbox
|
||||
POSTGRES_USER: netbox
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- netbox-postgres-data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --appendonly yes --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
environment:
|
||||
REDIS_PASSWORD: t4Ph722qJ5QHeQ1qfu36 #just for local dev
|
||||
volumes:
|
||||
- netbox-redis-data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
redis-cache:
|
||||
image: redis:6-alpine
|
||||
command:
|
||||
- sh
|
||||
- -c # this is to evaluate the $REDIS_PASSWORD from the env
|
||||
- redis-server --requirepass $$REDIS_PASSWORD ## $$ because of docker-compose
|
||||
environment:
|
||||
REDIS_PASSWORD: H733Kdjndks81 #just for local dev
|
||||
ports:
|
||||
- "6378:6379"
|
||||
|
||||
volumes:
|
||||
netbox-postgres-data:
|
||||
driver: local
|
||||
netbox-redis-data:
|
||||
driver: local
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
-78
@@ -1,78 +0,0 @@
|
||||
var gulp = require('gulp');
|
||||
var sass = require('gulp-sass');
|
||||
var concat = require('gulp-concat');
|
||||
var uglify = require('gulp-uglify');
|
||||
var cleanCss = require('gulp-clean-css');
|
||||
|
||||
|
||||
var jsVendorFiles = [
|
||||
'node_modules/vis-network/standalone/umd/vis-network.min.js',
|
||||
];
|
||||
|
||||
var cssVendorFiles = [
|
||||
'node_modules/vis-network/dist/dist/vis-network.min.css',
|
||||
];
|
||||
var sassFiles = './netbox_topology_views/static_dev/css/*.scss',
|
||||
cssDest = './netbox_topology_views/static/netbox_topology_views/css/';
|
||||
|
||||
var jsLocalResourceFiles = './netbox_topology_views/static_dev/js/*.js',
|
||||
jsDest = './netbox_topology_views/static/netbox_topology_views/js/';
|
||||
|
||||
|
||||
function styles_local() {
|
||||
return gulp.src(sassFiles)
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(concat('app.css'))
|
||||
.pipe(cleanCss())
|
||||
.pipe(gulp.dest(cssDest));
|
||||
|
||||
}
|
||||
exports.styles_local = styles_local
|
||||
|
||||
function styles_vendor() {
|
||||
return gulp.src(cssVendorFiles)
|
||||
.pipe(concat('vendor.css'))
|
||||
.pipe(cleanCss())
|
||||
.pipe(gulp.dest(cssDest))
|
||||
}
|
||||
exports.styles_vendor = styles_vendor
|
||||
|
||||
function js_local() {
|
||||
return gulp.src(jsLocalResourceFiles)
|
||||
.pipe(concat('app.js'))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(jsDest))
|
||||
}
|
||||
exports.js_local = js_local
|
||||
|
||||
function js_vendor() {
|
||||
return gulp.src(jsVendorFiles)
|
||||
.pipe(concat('vendor.js'))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(jsDest))
|
||||
|
||||
}
|
||||
exports.js_vendor = js_vendor
|
||||
|
||||
function js_local_dev() {
|
||||
return gulp.src(jsLocalResourceFiles)
|
||||
.pipe(concat('app.js'))
|
||||
.pipe(gulp.dest(jsDest))
|
||||
}
|
||||
exports.js_local_dev = js_local_dev
|
||||
|
||||
function js_vendor_dev() {
|
||||
return gulp.src(jsVendorFiles)
|
||||
.pipe(concat('vendor.js'))
|
||||
.pipe(gulp.dest(jsDest))
|
||||
|
||||
}
|
||||
exports.js_vendor_dev = js_vendor_dev
|
||||
|
||||
exports.css = gulp.series(styles_local, styles_vendor);
|
||||
|
||||
exports.js = gulp.series(js_local, js_vendor);
|
||||
exports.js_dev = gulp.series(js_local_dev, js_vendor_dev);
|
||||
|
||||
exports.build = gulp.series(exports.css, exports.js);
|
||||
exports.build_dev = gulp.series(exports.css, exports.js_dev);
|
||||
@@ -1,20 +1,36 @@
|
||||
from extras.plugins import PluginConfig
|
||||
|
||||
|
||||
class TopologyViewsConfig(PluginConfig):
|
||||
name = 'netbox_topology_views'
|
||||
verbose_name = 'Topology views'
|
||||
description = 'An plugin to render toplogoy maps'
|
||||
version = '0.4.2'
|
||||
author = 'Mattijs Vanhaverbeke'
|
||||
author_email = 'author@example.com'
|
||||
base_url = 'topology-views'
|
||||
name = "netbox_topology_views"
|
||||
verbose_name = "Topology views"
|
||||
description = "An plugin to render topology maps"
|
||||
version = "3.2.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',
|
||||
'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' : ''
|
||||
"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,
|
||||
"hide_single_cable_logical_conns": False,
|
||||
}
|
||||
|
||||
|
||||
config = TopologyViewsConfig
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
#no models yet
|
||||
@@ -0,0 +1 @@
|
||||
"""REST API"""
|
||||
@@ -1,24 +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 PreDeviceRoleSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ('id', 'name')
|
||||
|
||||
class PreTagSerializer(ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ('id', 'name')
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
from rest_framework import routers
|
||||
from netbox.api.routers import NetBoxRouter
|
||||
|
||||
from .views import PreSelectDeviceRolesViewSet, SearchViewSet, SaveCoordsViewSet, PreSelectTagsViewSet
|
||||
from netbox_topology_views.api import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
|
||||
router.register('preselecttags', PreSelectTagsViewSet)
|
||||
router.register('search', SearchViewSet, basename='search')
|
||||
router.register('save-coords', SaveCoordsViewSet, basename='save_coords')
|
||||
router = NetBoxRouter()
|
||||
|
||||
urlpatterns = router.urls
|
||||
router.register("save-coords", views.SaveCoordsViewSet)
|
||||
router.register("images", views.SaveRoleImageViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
+108
-143
@@ -1,163 +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.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||
|
||||
from .serializers import PreDeviceRoleSerializer, TopologyDummySerializer, PreTagSerializer
|
||||
from django.conf import settings
|
||||
|
||||
from utilities.api import IsAuthenticatedOrLoginNotRequired
|
||||
|
||||
from dcim.models import DeviceRole, Device, Cable
|
||||
from extras.models import Tag, CustomField, CustomFieldValue
|
||||
|
||||
ignore_cable_type_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["ignore_cable_type"]
|
||||
ignore_cable_type = ignore_cable_type_raw.split(",")
|
||||
|
||||
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
||||
preselected_device_roles = preselected_device_roles_raw.split(",")
|
||||
|
||||
preselected_tags_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_tags"]
|
||||
preselected_tags = preselected_tags_raw.split(",")
|
||||
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 PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
|
||||
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
|
||||
serializer_class = PreDeviceRoleSerializer
|
||||
|
||||
class PreSelectTagsViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Tag.objects.filter(name__in=preselected_tags)
|
||||
serializer_class = PreTagSerializer
|
||||
|
||||
class SaveCoordsViewSet(GenericViewSet):
|
||||
queryset = Device.objects.all()
|
||||
class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Device.objects.none()
|
||||
serializer_class = TopologyDummySerializer
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
@action(detail=False, methods=["patch"])
|
||||
def save_coords(self, request):
|
||||
results = {}
|
||||
if settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]:
|
||||
try:
|
||||
cfCoords = CustomField.objects.get(name='coordinates')
|
||||
except CustomField.DoesNotExist:
|
||||
results["status"] = "coords custom field not created"
|
||||
return Response(status=500)
|
||||
if not settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||
"allow_coordinates_saving"
|
||||
]:
|
||||
return Response({"status": "not allowed to save coords"}, status=500)
|
||||
|
||||
obj_type = ContentType.objects.get_for_model(Device)
|
||||
device_id: str = request.data.get("node_id", None)
|
||||
x_coord = request.data.get("x", None)
|
||||
y_coord = request.data.get("y", None)
|
||||
|
||||
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"]
|
||||
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)
|
||||
|
||||
actual_device= Device.objects.get(id=device_id)
|
||||
try:
|
||||
cfvCoords = CustomFieldValue.objects.get(obj_type=obj_type, obj_id=actual_device.pk, field__name='coordinates')
|
||||
cfvCoords.value = "%s;%s" % (x_coord,y_coord)
|
||||
cfvCoords.save()
|
||||
results["status"] = "coords set"
|
||||
except CustomFieldValue.DoesNotExist:
|
||||
cfvCoords = CustomFieldValue(field=cfCoords, obj_type=obj_type, obj_id=actual_device.id)
|
||||
cfvCoords.value = "%s;%s" % (x_coord,y_coord)
|
||||
cfvCoords.save()
|
||||
results["status"] = "coords set for first time"
|
||||
|
||||
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)
|
||||
|
||||
class SearchViewSet(GenericViewSet):
|
||||
#_ignore_model_permissions = True
|
||||
#permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
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
|
||||
)
|
||||
|
||||
queryset = Device.objects.all()
|
||||
serializer_class = TopologyDummySerializer
|
||||
|
||||
def _filter(self, site, role, name, tags):
|
||||
filter_devices = Device.objects.all()
|
||||
if name is not None:
|
||||
filter_devices = filter_devices.filter(name__contains=name)
|
||||
if site is not None:
|
||||
filter_devices = filter_devices.filter(site__id__in=site)
|
||||
if role is not None:
|
||||
filter_devices = filter_devices.filter(device_role__id__in=role)
|
||||
if tags is not None:
|
||||
filter_devices = filter_devices.filter(tags__id__in=tags)
|
||||
return filter_devices
|
||||
return Response({"status": "saved coords"})
|
||||
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def search(self, request):
|
||||
name = None
|
||||
sites = None
|
||||
devicerole = None
|
||||
tags = None
|
||||
if "devicerole" in request.data:
|
||||
if request.data["devicerole"]:
|
||||
devicerole = request.data["devicerole"]
|
||||
if "sites" in request.data:
|
||||
if request.data["sites"]:
|
||||
sites = request.data["sites"]
|
||||
if "name" in request.data:
|
||||
if request.data["name"]:
|
||||
name = request.data["name"]
|
||||
if "tags" in request.data:
|
||||
if request.data["tags"]:
|
||||
tags = request.data["tags"]
|
||||
class SaveRoleImageViewSet(PermissionRequiredMixin, ViewSet):
|
||||
queryset = DeviceRole.objects.none()
|
||||
serializer_class = RoleImageSerializer
|
||||
permission_required = (
|
||||
"dcim.add_device_role",
|
||||
"dcim.change_device_role",
|
||||
)
|
||||
|
||||
devices = self._filter(sites, devicerole, name, tags)
|
||||
def create(self, request):
|
||||
if not isinstance(request.data, dict):
|
||||
return JsonResponse(
|
||||
{"status": "Missing or malformed request body"}, status=400
|
||||
)
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
edge_ids = 0
|
||||
cable_ids = []
|
||||
for device in devices:
|
||||
cables = device.get_cables()
|
||||
for cable in cables:
|
||||
if cable.termination_a_type.name != "circuit termination" and cable.termination_b_type.name != "circuit termination":
|
||||
if cable.id not in cable_ids:
|
||||
if cable.termination_a_type.name not in ignore_cable_type and cable.termination_b_type.name not in ignore_cable_type:
|
||||
cable_ids.append(cable.id)
|
||||
edge_ids += 1
|
||||
edge = {}
|
||||
edge["id"] = edge_ids
|
||||
edge["from"] = cable.termination_a.device.id
|
||||
edge["to"] = cable.termination_b.device.id
|
||||
edge["title"] = "Connection between <br> " + cable.termination_a.device.name + " [" + cable.termination_a.name + "]<br>" + cable.termination_b.device.name + " [" + cable.termination_b.name + "]"
|
||||
edges.append(edge)
|
||||
else:
|
||||
pass
|
||||
#circuittermination not supported for now
|
||||
node = {}
|
||||
node["id"] = device.id
|
||||
node["name"] = device.name
|
||||
node["label"] = device.name + " " + device.device_type.display_name
|
||||
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"
|
||||
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()
|
||||
}
|
||||
|
||||
for device_custom_field in device.custom_field_values.all():
|
||||
if device_custom_field.field.name == "coordinates":
|
||||
cords = device_custom_field.serialized_value.split(";")
|
||||
node["x"] = int(cords[0])
|
||||
node["y"] = int(cords[1])
|
||||
node["physics"] = False
|
||||
|
||||
nodes.append(node)
|
||||
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
|
||||
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
|
||||
content_type_ids.keys()
|
||||
)
|
||||
|
||||
results = {}
|
||||
results["nodes"] = nodes
|
||||
results["edges"] = edges
|
||||
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,
|
||||
)
|
||||
|
||||
return Response(results)
|
||||
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"})
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import django_filters
|
||||
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.filtersets import TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
|
||||
|
||||
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method="search",
|
||||
label="Search",
|
||||
)
|
||||
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name="device_role_id",
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label="Role (ID)",
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name="site__region",
|
||||
lookup_expr="in",
|
||||
label="Region (ID)",
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
label="Site (ID)",
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name="location",
|
||||
lookup_expr="in",
|
||||
label="Location (ID)",
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ["id", "name"]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
"""Perform the filtered search."""
|
||||
if not value.strip():
|
||||
return queryset
|
||||
qs_filter = Q(name__icontains=value)
|
||||
return queryset.filter(qs_filter)
|
||||
@@ -0,0 +1,133 @@
|
||||
from cProfile import label
|
||||
import imp
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.models import Device, Site, Region, DeviceRole, Location, Rack
|
||||
|
||||
from django import forms
|
||||
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 utilities.forms import (
|
||||
TagFilterField,
|
||||
DynamicModelMultipleChoiceField,
|
||||
MultipleChoiceField,
|
||||
)
|
||||
|
||||
allow_coordinates_saving = bool(
|
||||
settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
|
||||
)
|
||||
|
||||
|
||||
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
model = Device
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
(
|
||||
"q",
|
||||
"filter_id",
|
||||
"hide_unconnected",
|
||||
"save_coords",
|
||||
"show_cables",
|
||||
"show_circuit",
|
||||
"show_logical_connections",
|
||||
"show_power",
|
||||
"show_wireless",
|
||||
),
|
||||
),
|
||||
(
|
||||
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")
|
||||
)
|
||||
device_role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=DeviceRole.objects.all(), required=False, label=_("Device Role")
|
||||
)
|
||||
id = DynamicModelMultipleChoiceField(
|
||||
queryset=Device.objects.all(),
|
||||
required=False,
|
||||
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",
|
||||
},
|
||||
label=_("Site"),
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
"region_id": "$region_id",
|
||||
"site_id": "$site_id",
|
||||
},
|
||||
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
|
||||
)
|
||||
show_logical_connections = forms.BooleanField(
|
||||
label =_("Show Logical Connections"), 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
|
||||
)
|
||||
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),
|
||||
)
|
||||
status = MultipleChoiceField(
|
||||
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'),
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,93 @@
|
||||
from django.db import models
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# no modeles yet
|
||||
from dcim.models import DeviceRole
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from netbox.models.features import (
|
||||
ChangeLoggingMixin,
|
||||
ExportTemplatesMixin,
|
||||
WebhooksMixin,
|
||||
)
|
||||
|
||||
from netbox_topology_views.utils import (
|
||||
CONF_IMAGE_DIR,
|
||||
IMAGE_DIR,
|
||||
Role,
|
||||
find_image_url,
|
||||
get_model_role,
|
||||
image_static_url,
|
||||
)
|
||||
|
||||
|
||||
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
]
|
||||
|
||||
objects: "models.Manager[RoleImage]"
|
||||
|
||||
image = models.CharField("Path within the netbox static directory", max_length=255)
|
||||
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(null=True, blank=True)
|
||||
|
||||
__role: Optional[Role] = None
|
||||
|
||||
@property
|
||||
def role(self) -> Role:
|
||||
if self.__role:
|
||||
return self.__role
|
||||
|
||||
model_class = self.content_type.model_class()
|
||||
|
||||
if not model_class:
|
||||
raise ValueError(f"Invalid content type: {self.content_type}")
|
||||
|
||||
if model_class == DeviceRole:
|
||||
device_role: DeviceRole = DeviceRole.objects.get(pk=self.object_id)
|
||||
self.__role = Role(slug=device_role.slug, name=device_role.name)
|
||||
return self.__role
|
||||
|
||||
self.__role = get_model_role(model_class)
|
||||
return self.__role
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.role} - {self.image}"
|
||||
|
||||
def get_image(self) -> Path:
|
||||
"""Get Icon
|
||||
|
||||
returns the model's image's absolute path in the filesystem
|
||||
raises ValueError if the file cannot be found
|
||||
"""
|
||||
path = Path(settings.STATIC_ROOT) / self.image
|
||||
|
||||
if not path.exists():
|
||||
raise ValueError(f"{self.role} path '{path}' does not exists")
|
||||
|
||||
return path
|
||||
|
||||
def get_default_image(self, dir: Path = CONF_IMAGE_DIR):
|
||||
"""Get default image
|
||||
|
||||
will attempt to find image in given directory with any file extension,
|
||||
otherwise will try to find a `role-unknown` image
|
||||
|
||||
fallback is `STATIC_ROOT/netbox_topology_views/img/role-unknown.png`
|
||||
"""
|
||||
if url := find_image_url(self.role.slug, dir):
|
||||
return url
|
||||
|
||||
# fallback to default role unknown image
|
||||
return image_static_url(IMAGE_DIR / "role-unknown.png")
|
||||
|
||||
def get_image_url(self, dir: Path = CONF_IMAGE_DIR) -> str:
|
||||
try:
|
||||
self.get_image()
|
||||
except ValueError:
|
||||
return self.get_default_image(dir)
|
||||
return static(f"/{self.image}")
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from extras.plugins import PluginMenuButton, PluginMenuItem
|
||||
from utilities.choices import ButtonColorChoices
|
||||
from extras.plugins import PluginMenu, PluginMenuItem
|
||||
|
||||
menu_items = (
|
||||
PluginMenuItem(
|
||||
link='plugins:netbox_topology_views:home',
|
||||
link_text='Topology',
|
||||
buttons=()
|
||||
menu = PluginMenu(
|
||||
label='Topology Views',
|
||||
groups=(
|
||||
('Topology', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
|
||||
('CUSTOMIZATION', (PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Topology Images"),),),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
#visgraph{height:70vh}
|
||||
#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
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
(()=>{var l=(e,t,o)=>new Promise((n,r)=>{var c=s=>{try{i(o.next(s))}catch(a){r(a)}},m=s=>{try{i(o.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,m);i((o=o.apply(e,t)).next())});var d=e=>{if(!document.cookie)return;let t=null,o=document.cookie.split(";");for(let n=0;n<o.length;n++){let r=o[n].trim();if(r.substring(0,e.length+1)===e+"="){t=decodeURIComponent(r.substring(e.length+1));break}}return t};var u={success:e=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()},error:e=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()}};var g={},p=d("csrftoken");document.querySelector("form#images").addEventListener("submit",e=>l(void 0,null,function*(){e.preventDefault();try{let t=yield fetch("/api/plugins/netbox_topology_views/images/",{method:"POST",body:JSON.stringify(g),headers:{"X-CSRFToken":p,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(e=>{e.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:o,image:n}}=t.currentTarget;g[o]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${o}`);r&&(r.innerHTML=`<img src="${n}" />`)})});})();
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
|
||||
"mappings": "mNAAO,GAAM,GAAY,AAAC,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,GAAI,GAAc,KACZ,EAAU,SAAS,OAAO,MAAM,KAEtC,OAAS,GAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,GAAM,GAAS,EAAQ,GAAG,OAE1B,GAAI,EAAO,UAAU,EAAG,EAAK,OAAS,KAAO,EAAO,IAAK,CACrD,EAAc,mBAAmB,EAAO,UAAU,EAAK,OAAS,IAChE,OAIR,MAAO,ICfJ,GAAM,GAAQ,CACjB,QAAS,AAAC,GAAY,CAClB,GAAM,GAAK,SAAS,cAAc,kCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,QAEV,MAAO,AAAC,GAAY,CAChB,GAAM,GAAK,SAAS,cAAc,gCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,SCZd,GAAM,GAAU,GACV,EAAY,EAAU,aAE5B,SAAS,cAAc,eAAe,iBAAiB,SAAU,AAAO,GAAM,0BAC1E,EAAE,iBACF,GAAI,CACA,GAAM,GAAM,KAAM,OAAM,6CAA8C,CAClE,OAAQ,OACR,KAAM,KAAK,UAAU,GACrB,QAAS,CACL,cAAe,EACf,eAAgB,sBAIxB,GAAI,CAAC,EAAI,GAAI,KAAM,IAAI,OAAM,KAAM,GAAI,QACvC,EAAM,QAAQ,wBACT,EAAP,CACE,QAAQ,IAAI,GACZ,EAAM,MAAM,EAAI,aAIxB,SAAS,iBAAiB,kCAAkC,QAAQ,AAAC,GAAO,CACxE,EAAG,iBAAiB,QAAS,AAAC,GAAM,CA3BxC,MA4BQ,GAAI,CAAE,GAAE,wBAAyB,cAAc,OAC/C,GAAM,CACF,QAAS,CAAE,OAAM,UACjB,EAAE,cAEN,EAAQ,GAAQ,EAEhB,GAAM,GAAS,KAAE,cACZ,QAAQ,eADE,cAET,cAAc,sBAAsB,KAC1C,AAAI,GAAQ,GAAO,UAAY,aAAa",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
|
||||
const esbuild = require('esbuild')
|
||||
const { sassPlugin } = require('esbuild-sass-plugin')
|
||||
|
||||
const options = {
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: 'external',
|
||||
sourcesContent: false,
|
||||
logLevel: 'error'
|
||||
}
|
||||
|
||||
const ARGS = process.argv.slice(2)
|
||||
const noCache = ARGS.includes('--no-cache')
|
||||
|
||||
async function bundleScripts() {
|
||||
const entryPoints = {
|
||||
app: 'js/home.js',
|
||||
images: 'js/images.js'
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await esbuild.build({
|
||||
...options,
|
||||
outdir: '../static/netbox_topology_views/js/',
|
||||
entryPoints,
|
||||
target: 'es2016'
|
||||
})
|
||||
if (result.errors.length !== 0) return
|
||||
|
||||
for (const [targetName, sourceName] of Object.entries(entryPoints)) {
|
||||
const source = sourceName.split('/').pop() // take last element
|
||||
console.log(
|
||||
`✅ Bundled source file '${source}' to '${targetName}.js'`
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function bundleStyles() {
|
||||
try {
|
||||
const entryPoints = {
|
||||
vendor: 'css/_external.scss',
|
||||
app: 'css/app.scss'
|
||||
}
|
||||
const pluginOptions = { outputStyle: 'compressed' }
|
||||
// Allow cache disabling.
|
||||
if (noCache) {
|
||||
pluginOptions.cache = false
|
||||
}
|
||||
|
||||
const result = await esbuild.build({
|
||||
...options,
|
||||
outdir: '../static/netbox_topology_views/css/',
|
||||
// Disable sourcemaps for CSS/SCSS files, see #7068
|
||||
sourcemap: false,
|
||||
entryPoints,
|
||||
plugins: [sassPlugin(pluginOptions)],
|
||||
loader: {
|
||||
'.eot': 'file',
|
||||
'.woff': 'file',
|
||||
'.woff2': 'file',
|
||||
'.svg': 'file',
|
||||
'.ttf': 'file'
|
||||
}
|
||||
})
|
||||
if (result.errors.length === 0) {
|
||||
for (const [targetName, sourceName] of Object.entries(
|
||||
entryPoints
|
||||
)) {
|
||||
const source = sourceName.split('/')[1]
|
||||
console.log(
|
||||
`✅ Bundled source file '${source}' to '${targetName}.css'`
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function bundleAll() {
|
||||
if (ARGS.includes('--styles')) {
|
||||
// Only run style jobs.
|
||||
return await bundleStyles()
|
||||
}
|
||||
if (ARGS.includes('--scripts')) {
|
||||
// Only run script jobs.
|
||||
return await bundleScripts()
|
||||
}
|
||||
await bundleStyles()
|
||||
await bundleScripts()
|
||||
}
|
||||
|
||||
bundleAll()
|
||||
@@ -0,0 +1 @@
|
||||
@import '../node_modules/vis-network/dist/dist/vis-network.min.css';
|
||||
@@ -1,3 +1,27 @@
|
||||
#visgraph {
|
||||
height: 70vh;
|
||||
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: 0.5rem;
|
||||
|
||||
padding-inline: 0.5rem;
|
||||
width: 50vw;
|
||||
max-width: 32rem;
|
||||
|
||||
> img {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const getCookie = (name) => {
|
||||
if (!document.cookie) return
|
||||
|
||||
let cookieValue = null
|
||||
const cookies = document.cookie.split(';')
|
||||
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim()
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === name + '=') {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return cookieValue
|
||||
}
|
||||
@@ -1,232 +1,155 @@
|
||||
var graph = null;
|
||||
var container = 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
|
||||
}
|
||||
},
|
||||
physics: {
|
||||
solver: 'forceAtlas2Based'
|
||||
}
|
||||
};
|
||||
|
||||
function iniPlotboxIndex() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
container = document.getElementById('visgraph');
|
||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
||||
startLoadSearchBar();
|
||||
handleButtonPress();
|
||||
}, false);
|
||||
}
|
||||
|
||||
function startLoadSearchBar() {
|
||||
$('#device-roles').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/dcim/device-roles/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#sites').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/dcim/sites/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
// Load CSRF token
|
||||
const csrftoken = getCookie('csrftoken')
|
||||
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#tags').select2({
|
||||
allowClear: true,
|
||||
placeholder: "---------",
|
||||
theme: "bootstrap",
|
||||
multiple: true,
|
||||
ajax: {
|
||||
url: "../../api/extras/tags/?brief=true",
|
||||
dataType: "json",
|
||||
type: "GET",
|
||||
data: function (params) {
|
||||
var queryParameters = {
|
||||
q: params.term
|
||||
}
|
||||
return queryParameters;
|
||||
},
|
||||
processResults: function (data) {
|
||||
return {
|
||||
results: $.map(data.results, function (item) {
|
||||
return {
|
||||
text: item.name,
|
||||
id: item.id
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
// Render vis graph
|
||||
let graph = null // vis graph instance
|
||||
|
||||
var deviceRolesSelect = $('#device-roles');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '../../api/plugins/topology-views/preselectdeviceroles/'
|
||||
}).then(function (data) {
|
||||
$.each(data.results, function (index, device_role_to_preload) {
|
||||
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
|
||||
deviceRolesSelect.append(option).trigger('change');
|
||||
deviceRolesSelect.trigger({
|
||||
type: 'select2:select',
|
||||
params: {
|
||||
data: device_role_to_preload
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const container = document.querySelector('#visgraph')
|
||||
const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||
;(function handleLoadData() {
|
||||
if (!topologyData) return
|
||||
|
||||
var tagsSelect = $('#tags');
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '../../api/plugins/topology-views/preselecttags/'
|
||||
}).then(function (data) {
|
||||
$.each(data.results, function (index, tags_to_preload) {
|
||||
var option = new Option(tags_to_preload.name, tags_to_preload.id, true, true);
|
||||
tagsSelect.append(option).trigger('change');
|
||||
tagsSelect.trigger({
|
||||
type: 'select2:select',
|
||||
params: {
|
||||
data: tags_to_preload
|
||||
function htmlTitle(text) {
|
||||
const container = document.createElement('div')
|
||||
container.innerHTML = text
|
||||
return container
|
||||
}
|
||||
|
||||
const nodes = new DataSet(
|
||||
topologyData.nodes.map((node) => ({
|
||||
...node,
|
||||
title: htmlTitle(node.title)
|
||||
}))
|
||||
)
|
||||
|
||||
const edges = new DataSet(
|
||||
topologyData.edges.map((node) => ({
|
||||
...node,
|
||||
title: htmlTitle(node.title)
|
||||
}))
|
||||
)
|
||||
graph = new Network(container, { nodes, edges }, options)
|
||||
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]) => {
|
||||
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
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
console.log(nodeId, res.status, res.statusText)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
graph.on('doubleClick', (params) => {
|
||||
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')
|
||||
})
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
// Download Graph
|
||||
const MIME_TYPE = 'image/png'
|
||||
|
||||
const downloadButton = document.querySelector('#btnDownloadImage')
|
||||
downloadButton.addEventListener('click', (e) => {
|
||||
performGraphDownload()
|
||||
})
|
||||
|
||||
function performGraphDownload() {
|
||||
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)
|
||||
}
|
||||
|
||||
function handleButtonPress() {
|
||||
$("#search-form").submit(function (event) {
|
||||
$("#status").html('<span class="label label-info">Loading data</span>');
|
||||
event.preventDefault();
|
||||
var value = $("#name").val();
|
||||
var value2 = $("#device-roles").val();
|
||||
var value3 = $("#sites").val();
|
||||
var value4 = $("#tags").val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "../../api/plugins/topology-views/search/search/",
|
||||
data: JSON.stringify({
|
||||
'name': value,
|
||||
'devicerole': value2,
|
||||
'sites': value3,
|
||||
'tags': value4
|
||||
}),
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
success: function (data_result) {
|
||||
$("#status").html('<span class="label label-info">Drawing network</span>');
|
||||
graph = null;
|
||||
nodes = new vis.DataSet();
|
||||
edges = new vis.DataSet();
|
||||
graph = new vis.Network(container, { nodes: nodes, edges: edges }, options);
|
||||
$.each(data_result["nodes"], function (index, device) {
|
||||
nodes.add(device);
|
||||
});
|
||||
$.each(data_result["edges"], function (index, edge) {
|
||||
edges.add(edge);
|
||||
});
|
||||
graph.fit();
|
||||
// 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.on('afterDrawing', function () {
|
||||
$("#status").html('<span class="label label-success">Ready</span>');
|
||||
});
|
||||
|
||||
graph.on("dragEnd", function (params) {
|
||||
dragged = this.getPositions(params.nodes);
|
||||
$.each(dragged, function (node_id, coordinates) {
|
||||
$("#coordstatus").html('');
|
||||
if ($('#checkSaveCoordinates').is(":checked")) {
|
||||
//nodes.update({ id: node_id, physics: false });
|
||||
$.ajax({
|
||||
url: "../../api/plugins/topology-views/save-coords/save_coords/",
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: { "X-CSRFToken": csrftoken },
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
'node_id': node_id,
|
||||
'x': coordinates.x,
|
||||
'y': coordinates.y
|
||||
}),
|
||||
error: function (error_result) {
|
||||
$("#coordstatus").html('<span class="label label-warning">Failed to update coordinates</span>');
|
||||
},
|
||||
success: function (data_result) {
|
||||
$("#coordstatus").html('<span class="label label-success">Updated coordinates</span>');
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
error: function (error_result) {
|
||||
$("#status").html('<span class="label label-warning">Something went wrong</span>');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
+791
@@ -0,0 +1,791 @@
|
||||
{
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.2.0",
|
||||
"dependencies": {
|
||||
"vis-data": "^7.1.4",
|
||||
"vis-network": "^9.1.2",
|
||||
"vis-util": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0",
|
||||
"esbuild": "^0.12.24",
|
||||
"esbuild-sass-plugin": "^1.5.2",
|
||||
"keycharm": "^0.4.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/hammerjs": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
|
||||
"integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==",
|
||||
"dependencies": {
|
||||
"@types/hammerjs": "^2.0.36"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@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==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
|
||||
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/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=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.12.29",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz",
|
||||
"integrity": "sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.14.5",
|
||||
"picomatch": "^2.3.0",
|
||||
"resolve": "^1.20.0",
|
||||
"sass": "^1.45.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sass-plugin/node_modules/esbuild": {
|
||||
"version": "0.14.54",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz",
|
||||
"integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"has": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/keycharm": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz",
|
||||
"integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ=="
|
||||
},
|
||||
"node_modules/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,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
"integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.9.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.56.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz",
|
||||
"integrity": "sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/timsort": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
|
||||
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A=="
|
||||
},
|
||||
"node_modules/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,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"uuid": "^7.0.0 || ^8.0.0",
|
||||
"vis-util": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vis-network": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz",
|
||||
"integrity": "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0",
|
||||
"keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0",
|
||||
"timsort": "^0.3.0",
|
||||
"uuid": "^3.4.0 || ^7.0.0 || ^8.0.0",
|
||||
"vis-data": "^7.0.0",
|
||||
"vis-util": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/visjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@egjs/hammerjs": "^2.0.0",
|
||||
"component-emitter": "^1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "3.2.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
# 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.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz"
|
||||
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
|
||||
dependencies:
|
||||
"@types/hammerjs" "^2.0.36"
|
||||
|
||||
"@types/hammerjs@^2.0.36":
|
||||
version "2.0.41"
|
||||
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.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.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.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.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
|
||||
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.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz"
|
||||
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
|
||||
|
||||
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.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz"
|
||||
integrity sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==
|
||||
dependencies:
|
||||
esbuild "^0.14.5"
|
||||
picomatch "^2.3.0"
|
||||
resolve "^1.20.0"
|
||||
sass "^1.45.0"
|
||||
|
||||
esbuild@^0.12.24:
|
||||
version "0.12.29"
|
||||
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz"
|
||||
integrity sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==
|
||||
|
||||
esbuild@^0.14.5:
|
||||
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.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.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
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.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.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.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.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.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.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.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.npmjs.org/is-number/-/is-number-7.0.0.tgz"
|
||||
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
|
||||
|
||||
"keycharm@^0.2.0 || ^0.3.0 || ^0.4.0", keycharm@^0.4.0:
|
||||
version "0.4.0"
|
||||
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.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.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.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
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.npmjs.org/resolve/-/resolve-1.22.1.tgz"
|
||||
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.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"
|
||||
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.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.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.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.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@^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.npmjs.org/uuid/-/uuid-8.3.2.tgz"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
vis-data@^7.0.0, vis-data@^7.1.4:
|
||||
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@^9.1.2:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz"
|
||||
integrity sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==
|
||||
|
||||
"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.npmjs.org/vis-util/-/vis-util-5.0.3.tgz"
|
||||
integrity sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -1,81 +1,79 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'base/layout.html' %}
|
||||
{% load buttons %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load static %}
|
||||
{% load perms %}
|
||||
{% load helpers %}
|
||||
|
||||
{% block content %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
{% block title %}Topology Views{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Network</strong> <span id="status"></span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="visgraph" class=""></div>
|
||||
</div>
|
||||
</div>
|
||||
{% block controls %}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Search</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form id="search-form" class="form" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" class="form-control" type="text">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-roles">Device roles</label>
|
||||
<select class="form-control" multiple="multiple" id="device-roles"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sites">Sites</label>
|
||||
<select class="form-control" multiple="multiple" id="sites"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags</label>
|
||||
<select class="form-control" multiple="multiple" id="tags"></select>
|
||||
</div>
|
||||
<hr class="mb-4">
|
||||
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock controls %}
|
||||
|
||||
</div>
|
||||
{% block tabs %}
|
||||
<ul class="nav nav-tabs px-3">
|
||||
{% block tab_items %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
|
||||
Network
|
||||
</button>
|
||||
</li>
|
||||
{% if filter_form %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
|
||||
Filters
|
||||
{% if filter_form %}{% badge filter_form.changed_data|length bg_color="blue" %}{% endif %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock tab_items %}
|
||||
</ul>
|
||||
{% endblock tabs %}
|
||||
|
||||
{% if config.allow_coordinates_saving == True %}
|
||||
<div class="panel panel-default mt-3">
|
||||
<div class="panel-heading">
|
||||
<strong>Settings</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="checkSaveCoordinates">
|
||||
<label class="custom-control-label" for="checkSaveCoordinates">Save coordinates</label>
|
||||
</div>
|
||||
<div>
|
||||
<span id="coordstatus"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% block content-wrapper %}
|
||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||
<div class="tab-content">
|
||||
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
{# 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if filter_form %}
|
||||
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock content-wrapper %}
|
||||
|
||||
{% block javascript %}
|
||||
|
||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
||||
|
||||
<script type="application/javascript"> iniPlotboxIndex()</script>
|
||||
|
||||
{% endblock %}
|
||||
<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 }}&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>
|
||||
@@ -0,0 +1,44 @@
|
||||
<div id="django-messages" class="toast-container">
|
||||
<div
|
||||
id="topology-plugin-error-toast"
|
||||
class="toast align-items-center border-0 bg-danger hide"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
data-bs-delay="2000"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
😢
|
||||
<span>Error</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close me-2 m-auto"
|
||||
data-bs-dismiss="toast"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="topology-plugin-success-toast"
|
||||
class="toast align-items-center border-0 bg-success hide"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
data-bs-delay="2000"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
👍
|
||||
<span>Success</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close me-2 m-auto"
|
||||
data-bs-dismiss="toast"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +1,12 @@
|
||||
from django.urls import path
|
||||
from django.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"),
|
||||
)
|
||||
|
||||
@@ -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__),
|
||||
)
|
||||
@@ -1,12 +1,769 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.views.generic import View
|
||||
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,
|
||||
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 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}
|
||||
)
|
||||
|
||||
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__)
|
||||
)
|
||||
|
||||
|
||||
def create_node(
|
||||
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool
|
||||
):
|
||||
node = {}
|
||||
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["shape"] = "image"
|
||||
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(";")
|
||||
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: int,
|
||||
termination_a: Dict,
|
||||
termination_b: Dict,
|
||||
circuit: Optional[Dict] = None,
|
||||
cable: Optional[Cable] = None,
|
||||
wireless: Optional[Dict] = None,
|
||||
power: Optional[bool] = None,
|
||||
interface: 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"]
|
||||
title = "Cable"
|
||||
|
||||
if circuit is not None:
|
||||
edge["dashes"] = True
|
||||
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"
|
||||
|
||||
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[
|
||||
"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:
|
||||
edge["href"] = cable.get_absolute_url()
|
||||
if hasattr(cable, 'color') and cable.color != "":
|
||||
edge["color"] = "#" + cable.color
|
||||
|
||||
return edge
|
||||
|
||||
|
||||
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_cables: bool,
|
||||
show_circuit: bool,
|
||||
show_logical_connections: bool,
|
||||
show_power: bool,
|
||||
show_wireless: bool,
|
||||
):
|
||||
if not queryset:
|
||||
return None
|
||||
|
||||
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)
|
||||
interface_ids = DefaultDict(dict)
|
||||
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||
"ignore_cable_type"
|
||||
]
|
||||
hide_single_cable_logical_conns = bool(
|
||||
settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||
"hide_single_cable_logical_conns"
|
||||
]
|
||||
)
|
||||
|
||||
device_ids = [d.pk for d in queryset]
|
||||
site_ids = [d.site_id for d in queryset]
|
||||
|
||||
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,
|
||||
cable=circuit_termination.cable,
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
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 hide_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=True))
|
||||
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:
|
||||
nodes_devices[qs_device.pk] = qs_device
|
||||
|
||||
results = {}
|
||||
|
||||
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')
|
||||
permission_required = ("dcim.view_site", "dcim.view_device")
|
||||
|
||||
"""
|
||||
Show the home page
|
||||
"""
|
||||
def get(self, request):
|
||||
return render(request, 'netbox_topology_views/index.html')
|
||||
|
||||
def get(self, request):
|
||||
self.filterset = DeviceFilterSet
|
||||
self.queryset = Device.objects.all().select_related(
|
||||
"device_type", "device_role"
|
||||
)
|
||||
self.queryset = self.filterset(request.GET, self.queryset).qs
|
||||
self.model = self.queryset.model
|
||||
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":
|
||||
hide_unconnected = True
|
||||
|
||||
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
|
||||
|
||||
show_logical_connections = False
|
||||
if "show_logical_connections" in request.GET:
|
||||
if request.GET["show_logical_connections"] == "on" :
|
||||
show_logical_connections = 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 "draw_init" in request.GET:
|
||||
if request.GET["draw_init"].lower() == "true":
|
||||
topo_data = get_topology_data(
|
||||
self.queryset,
|
||||
hide_unconnected,
|
||||
save_coords,
|
||||
show_cables,
|
||||
show_circuit,
|
||||
show_logical_connections,
|
||||
show_power,
|
||||
show_wireless,
|
||||
)
|
||||
else:
|
||||
topo_data = get_topology_data(
|
||||
self.queryset,
|
||||
hide_unconnected,
|
||||
save_coords,
|
||||
show_cables,
|
||||
show_circuit,
|
||||
show_logical_connections,
|
||||
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
|
||||
)
|
||||
|
||||
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"
|
||||
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",
|
||||
{
|
||||
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
|
||||
"topology_data": json.dumps(topo_data),
|
||||
"broken_image": find_image_url("role-unknown"),
|
||||
"model": self.model,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
CONFIG = settings.PLUGINS_CONFIG["netbox_topology_views"]
|
||||
ADDITIONAL_ROLES = (PowerPanel, PowerFeed, Circuit)
|
||||
|
||||
|
||||
class TopologyImagesView(PermissionRequiredMixin, View):
|
||||
permission_required = (
|
||||
"dcim.view_site",
|
||||
"dcim.view_device_role",
|
||||
"dcim.add_device_role",
|
||||
"dcim.change_device_role",
|
||||
)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
images = [
|
||||
{"url": image_static_url(image), "title": image.stem}
|
||||
for image in CONF_IMAGE_DIR.iterdir()
|
||||
]
|
||||
|
||||
roles = reduce(
|
||||
lambda acc, cur: {
|
||||
**acc,
|
||||
cur.name: {
|
||||
"id": cur.pk,
|
||||
"name": cur.name,
|
||||
"slug": cur.slug,
|
||||
"image": find_image_url(cur.slug),
|
||||
},
|
||||
},
|
||||
DeviceRole.objects.all(),
|
||||
dict(),
|
||||
)
|
||||
|
||||
for additional_role in ADDITIONAL_ROLES:
|
||||
cur = get_model_role(additional_role)
|
||||
ct = ContentType.objects.get_for_model(additional_role).pk
|
||||
|
||||
roles[cur.name] = {
|
||||
"id": f"ct{ct}",
|
||||
"name": cur.name,
|
||||
"slug": cur.slug,
|
||||
"image": find_image_url(cur.slug),
|
||||
}
|
||||
|
||||
role_images = RoleImage.objects.all()
|
||||
|
||||
for role_image in role_images:
|
||||
roles[role_image.role.name]["image"] = role_image.get_image_url()
|
||||
|
||||
return render(
|
||||
request,
|
||||
"netbox_topology_views/images.html",
|
||||
{
|
||||
"roles": sorted(list(roles.values()), key=lambda r: r["name"]),
|
||||
"images": images,
|
||||
},
|
||||
)
|
||||
|
||||
Generated
-4118
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "netbox_topology_views",
|
||||
"version": "0.4.2",
|
||||
"scripts": {
|
||||
"resources": "gulp build",
|
||||
"resources_dev": "gulp build_dev"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"vis-network": "^7.6.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
djangorestframework
|
||||
django-filter
|
||||
black
|
||||
@@ -1,22 +1,29 @@
|
||||
from setuptools import setup, find_packages
|
||||
from pathlib import Path
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
readme = Path(__file__).parent / "README.md"
|
||||
long_description = readme.read_text()
|
||||
|
||||
setup(
|
||||
name='netbox-topology-views',
|
||||
version='0.4.2',
|
||||
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.2.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",
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user