Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a57e1f79ab | ||
|
|
6e142bdb74 | ||
|
|
79cb8d8e2e | ||
|
|
1c9d4d8a1e | ||
|
|
6fc32b9bfa | ||
|
|
e0624effc2 | ||
|
|
fe94765c15 | ||
|
|
ffd91e2cf5 | ||
|
|
44e82c0286 | ||
|
|
33d7157b12 | ||
|
|
5c4c6be745 | ||
|
|
5e53066ba7 | ||
|
|
a1d155fdbc | ||
|
|
ab944fdcef | ||
|
|
f72ca57ac7 | ||
|
|
8a6f1660f0 | ||
|
|
d608d63207 | ||
|
|
4d161fe6ac | ||
|
|
c6a6893c10 | ||
|
|
b2430d1723 |
@@ -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:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -13,19 +13,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.x'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install setuptools wheel twine
|
pip install build
|
||||||
- name: Build and publish
|
- name: Build package
|
||||||
env:
|
run: python -m build
|
||||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
- name: Publish package
|
||||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
run: |
|
with:
|
||||||
python setup.py sdist bdist_wheel
|
user: __token__
|
||||||
twine upload dist/*
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
|||||||
+5
-1
@@ -110,6 +110,9 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# VS Code / VS Codium project settings
|
||||||
|
.vscode/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
@@ -128,4 +131,5 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
app.js.map
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -1,17 +1,155 @@
|
|||||||
# netbox-topology-views
|
# Netbox Topology Views Plugin
|
||||||
|
|
||||||
## preview
|
 
|
||||||
|
|
||||||

|
Create topology views/maps from your devices in netbox.
|
||||||
|
The connections are based on the cables you created in netbox.
|
||||||
|
Support to filter on name, site, tag and device role.
|
||||||
|
|
||||||
## install
|
## Preview
|
||||||
|
|
||||||
Add 'netbox_topology_views' to the PLUGINS array in your django configuration file.
|

|
||||||
Then run python3 manage.py collectstatic --no-input
|
|
||||||
|
|
||||||
There is also support for custom fiels.
|
## Install
|
||||||
If you create a custom field "coordinates" for "dcim > device" with type "text" and name "coordinates" you will see the same layout every time.
|
|
||||||
|
|
||||||
## use
|
**_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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```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 fields.
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
PLUGINS_CONFIG = {
|
||||||
|
'netbox_topology_views': {
|
||||||
|
'preselected_device_roles': ['Router', 'Firewall'],
|
||||||
|
'enable_circuit_terminations': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 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
|
||||||
|
|
||||||
|
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/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)
|
||||||
|
|
||||||
go to /plugins/topology-views/ view your topologies
|
|
||||||
+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/styles/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,18 +1,36 @@
|
|||||||
from extras.plugins import PluginConfig
|
from extras.plugins import PluginConfig
|
||||||
|
|
||||||
|
|
||||||
class TopologyViewsConfig(PluginConfig):
|
class TopologyViewsConfig(PluginConfig):
|
||||||
name = 'netbox_topology_views'
|
name = "netbox_topology_views"
|
||||||
verbose_name = 'Topology views'
|
verbose_name = "Topology views"
|
||||||
description = 'An plugin to render toplogoy maps'
|
description = "An plugin to render topology maps"
|
||||||
version = '0.2'
|
version = "3.2.0"
|
||||||
author = 'Mattijs Vanhaverbeke'
|
author = "Mattijs Vanhaverbeke"
|
||||||
author_email = 'author@example.com'
|
author_email = "author@example.com"
|
||||||
base_url = 'topology-views'
|
base_url = "netbox_topology_views"
|
||||||
required_settings = []
|
required_settings = []
|
||||||
default_settings = {
|
default_settings = {
|
||||||
'preselected_device_roles': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
|
"preselected_device_roles": [
|
||||||
'IGNORE_CABLE_TYPE': 'dcim.poweroutlet,dcim.powerport',
|
"Firewall",
|
||||||
'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'
|
"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
|
config = TopologyViewsConfig
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
#no models yet
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""REST API"""
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
|
from dcim.models import Device, DeviceRole
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
|
||||||
from dcim.models import DeviceRole
|
from netbox_topology_views.models import RoleImage
|
||||||
|
|
||||||
class PreDeviceRoleSerializer(ModelSerializer):
|
|
||||||
|
|
||||||
|
class TopologyDummySerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Device
|
||||||
|
fields = ("id", "name")
|
||||||
|
|
||||||
|
|
||||||
|
class RoleImageSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = RoleImage
|
||||||
|
fields = ("role", "image")
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRoleSerializer(ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceRole
|
model = DeviceRole
|
||||||
fields = ('id', 'name')
|
fields = ("name", "slug", "color", "vm_role", "description")
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from rest_framework import routers
|
from netbox.api.routers import NetBoxRouter
|
||||||
|
|
||||||
from .views import PreSelectDeviceRolesViewSet, SearchViewSet
|
from netbox_topology_views.api import views
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = NetBoxRouter()
|
||||||
router.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
|
|
||||||
router.register('search', SearchViewSet, basename='search')
|
|
||||||
|
|
||||||
urlpatterns = router.urls
|
router.register("save-coords", views.SaveCoordsViewSet)
|
||||||
|
router.register("images", views.SaveRoleImageViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
|
|||||||
@@ -1,89 +1,128 @@
|
|||||||
from rest_framework.viewsets import ModelViewSet, ViewSet, ReadOnlyModelViewSet
|
from typing import Dict
|
||||||
|
|
||||||
|
from circuits.models import Circuit
|
||||||
|
from dcim.models import Device, DeviceRole, PowerFeed, PowerPanel
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.http import JsonResponse
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
|
||||||
|
|
||||||
from .serializers import PreDeviceRoleSerializer
|
from netbox_topology_views.api.serializers import (
|
||||||
from django.conf import settings
|
RoleImageSerializer,
|
||||||
|
TopologyDummySerializer,
|
||||||
from utilities.api import IsAuthenticatedOrLoginNotRequired
|
)
|
||||||
|
from netbox_topology_views.models import RoleImage
|
||||||
from dcim.models import DeviceRole, Device, Cable
|
from netbox_topology_views.utils import get_image_from_url
|
||||||
|
|
||||||
|
|
||||||
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
|
class SaveCoordsViewSet(ReadOnlyModelViewSet):
|
||||||
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
|
queryset = Device.objects.none()
|
||||||
preselected_device_roles = preselected_device_roles_raw.split(",")
|
serializer_class = TopologyDummySerializer
|
||||||
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
|
|
||||||
serializer_class = PreDeviceRoleSerializer
|
|
||||||
|
|
||||||
class SearchViewSet(ViewSet):
|
@action(detail=False, methods=["patch"])
|
||||||
_ignore_model_permissions = True
|
def save_coords(self, request):
|
||||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
if not settings.PLUGINS_CONFIG["netbox_topology_views"][
|
||||||
|
"allow_coordinates_saving"
|
||||||
|
]:
|
||||||
|
return Response({"status": "not allowed to save coords"}, status=500)
|
||||||
|
|
||||||
def _filter(self, site, role, name):
|
device_id: str = request.data.get("node_id", None)
|
||||||
filter_devices = Device.objects.all()
|
x_coord = request.data.get("x", None)
|
||||||
if name is not None:
|
y_coord = request.data.get("y", None)
|
||||||
filter_devices = filter_devices.filter(name__contains=name)
|
|
||||||
if site is not None:
|
actual_device = None
|
||||||
filter_devices = filter_devices.filter(site__id__in=site)
|
if device_id.startswith("c"):
|
||||||
if role is not None:
|
device_id = device_id.lstrip("c")
|
||||||
filter_devices = filter_devices.filter(device_role__id__in=role)
|
actual_device = Circuit.objects.get(id=device_id)
|
||||||
return filter_devices
|
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)
|
||||||
|
|
||||||
|
if not actual_device:
|
||||||
|
return Response({"status": "invalid node_id in body"}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
actual_device.custom_field_data["coordinates"] = "%s;%s" % (
|
||||||
|
x_coord,
|
||||||
|
y_coord,
|
||||||
|
)
|
||||||
|
actual_device.save()
|
||||||
|
except:
|
||||||
|
return Response(
|
||||||
|
{"status": "coords custom field could not be saved"}, status=500
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({"status": "saved coords"})
|
||||||
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
class SaveRoleImageViewSet(PermissionRequiredMixin, ViewSet):
|
||||||
def search(self, request):
|
queryset = DeviceRole.objects.none()
|
||||||
name = None
|
serializer_class = RoleImageSerializer
|
||||||
sites = None
|
permission_required = (
|
||||||
devicerole = None
|
"dcim.add_device_role",
|
||||||
if "devicerole" in request.data:
|
"dcim.change_device_role",
|
||||||
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"]
|
|
||||||
|
|
||||||
devices = self._filter(sites, devicerole, name)
|
def create(self, request):
|
||||||
nodes = []
|
if not isinstance(request.data, dict):
|
||||||
edges = []
|
return JsonResponse(
|
||||||
edge_ids = 0
|
{"status": "Missing or malformed request body"}, status=400
|
||||||
cable_ids = []
|
)
|
||||||
for device in devices:
|
|
||||||
cables = device.get_cables()
|
|
||||||
for cable in cables:
|
|
||||||
if cable.id not in cable_ids:
|
|
||||||
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)
|
|
||||||
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"
|
|
||||||
|
|
||||||
for device_custom_field in device.custom_field_values.all():
|
device_roles = {k: v for k, v in request.data.items() if k.isnumeric()}
|
||||||
if device_custom_field.field.name == "coordinates":
|
content_type_ids = {
|
||||||
cords = device_custom_field.serialized_value.split(";")
|
k[2:]: v
|
||||||
node["x"] = int(cords[0])
|
for k, v in request.data.items()
|
||||||
node["y"] = int(cords[1])
|
if k.startswith("ct") and k[2:].isnumeric()
|
||||||
node["physics"] = False
|
}
|
||||||
|
|
||||||
nodes.append(node)
|
|
||||||
|
|
||||||
results = {}
|
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
|
||||||
results["nodes"] = nodes
|
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
|
||||||
results["edges"] = edges
|
content_type_ids.keys()
|
||||||
|
)
|
||||||
|
|
||||||
return Response(results)
|
if len(roles) != len(device_roles):
|
||||||
|
difference = set(device_roles) - set(roles.keys())
|
||||||
|
return JsonResponse(
|
||||||
|
{"status": f"Got unknown device role ids: {difference}"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(content_types) != len(content_type_ids):
|
||||||
|
difference = set(content_type_ids) - set(content_types.keys())
|
||||||
|
return JsonResponse(
|
||||||
|
{"status": f"Got unknown content type ids: {difference}"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if device_roles:
|
||||||
|
device_role_ct = ContentType.objects.get_for_model(DeviceRole)
|
||||||
|
|
||||||
|
for id, url in device_roles.items():
|
||||||
|
RoleImage.objects.update_or_create(
|
||||||
|
{
|
||||||
|
"content_type_id": device_role_ct.pk,
|
||||||
|
"object_id": id,
|
||||||
|
"image": str(get_image_from_url(url)),
|
||||||
|
},
|
||||||
|
object_id=id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for content_type_id, url in content_type_ids.items():
|
||||||
|
RoleImage.objects.update_or_create(
|
||||||
|
{
|
||||||
|
"content_type_id": content_type_id,
|
||||||
|
"image": str(get_image_from_url(url)),
|
||||||
|
},
|
||||||
|
content_type_id=content_type_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({"status": "Ok"})
|
||||||
|
|||||||
@@ -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 extras.plugins import PluginMenu, PluginMenuItem
|
||||||
from utilities.choices import ButtonColorChoices
|
|
||||||
|
|
||||||
menu_items = (
|
menu = PluginMenu(
|
||||||
PluginMenuItem(
|
label='Topology Views',
|
||||||
link='plugins:netbox_topology_views:home',
|
groups=(
|
||||||
link_text='Topology',
|
('Topology', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
|
||||||
buttons=()
|
('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 {
|
#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,9 +1,8 @@
|
|||||||
var graph = null;
|
import { DataSet } from 'vis-data/esnext'
|
||||||
var container = null;
|
import { Network } from 'vis-network/esnext'
|
||||||
var csrftoken = null;
|
import { getCookie } from './csrftoken.js'
|
||||||
var nodes = new vis.DataSet();
|
|
||||||
var edges = new vis.DataSet();
|
const options = {
|
||||||
var options = {
|
|
||||||
interaction: {
|
interaction: {
|
||||||
hover: true,
|
hover: true,
|
||||||
hoverConnectedEdges: true,
|
hoverConnectedEdges: true,
|
||||||
@@ -11,184 +10,146 @@ var options = {
|
|||||||
},
|
},
|
||||||
nodes: {
|
nodes: {
|
||||||
shape: 'image',
|
shape: 'image',
|
||||||
brokenImage: '/static/netbox_topology_views/img/role-unknown.png',
|
brokenImage: brokenImage ?? '',
|
||||||
size: 35,
|
size: 35,
|
||||||
font: {
|
font: {
|
||||||
multi: 'md',
|
multi: 'md',
|
||||||
face: 'helvetica',
|
face: 'helvetica',
|
||||||
},
|
color:
|
||||||
|
document.documentElement.dataset.netboxColorMode === 'dark'
|
||||||
|
? '#fff'
|
||||||
|
: '#000'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
edges: {
|
edges: {
|
||||||
length: 100,
|
length: 100,
|
||||||
width: 2,
|
width: 2,
|
||||||
font: {
|
font: {
|
||||||
face: 'helvetica',
|
face: 'helvetica'
|
||||||
},
|
},
|
||||||
|
shadow: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
physics: {
|
physics: {
|
||||||
solver: 'forceAtlas2Based'
|
solver: 'forceAtlas2Based'
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function iniPlotboxIndex() {
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
container = document.getElementById('visgraph');
|
|
||||||
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
|
|
||||||
startLoadSearchBar();
|
|
||||||
handleButtonPress();
|
|
||||||
}, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLoadSearchBar() {
|
// Load CSRF token
|
||||||
$('#device-roles').select2({
|
const csrftoken = getCookie('csrftoken')
|
||||||
allowClear: true,
|
|
||||||
placeholder: "---------",
|
|
||||||
theme: "bootstrap",
|
|
||||||
multiple: true,
|
|
||||||
ajax: {
|
|
||||||
url: "/api/dcim/device-roles/?brief=true",
|
|
||||||
dataType: "json",
|
|
||||||
type: "GET",
|
|
||||||
data: function (params) {
|
|
||||||
var queryParameters = {
|
|
||||||
term: 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) {
|
|
||||||
|
|
||||||
var queryParameters = {
|
// Render vis graph
|
||||||
term: params.term
|
let graph = null // vis graph instance
|
||||||
}
|
|
||||||
return queryParameters;
|
|
||||||
},
|
|
||||||
processResults: function (data) {
|
|
||||||
return {
|
|
||||||
results: $.map(data.results, function (item) {
|
|
||||||
return {
|
|
||||||
text: item.name,
|
|
||||||
id: item.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var deviceRolesSelect = $('#device-roles');
|
const container = document.querySelector('#visgraph')
|
||||||
$.ajax({
|
const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
||||||
type: 'GET',
|
;(function handleLoadData() {
|
||||||
url: '/api/plugins/topology-views/preselectdeviceroles/'
|
if (!topologyData) return
|
||||||
}).then(function (data) {
|
|
||||||
$.each(data.results, function (index, device_role_to_preload) {
|
function htmlTitle(text) {
|
||||||
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
|
const container = document.createElement('div')
|
||||||
deviceRolesSelect.append(option).trigger('change');
|
container.innerHTML = text
|
||||||
deviceRolesSelect.trigger({
|
return container
|
||||||
type: 'select2:select',
|
}
|
||||||
params: {
|
|
||||||
data: device_role_to_preload
|
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() {
|
// Theme switching
|
||||||
$("#search-form").submit(function (event) {
|
const observer = new MutationObserver((mutations) =>
|
||||||
$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>');
|
mutations.forEach((mutation) => {
|
||||||
event.preventDefault();
|
if (
|
||||||
var value = $("#name").val();
|
!graph ||
|
||||||
var value2 = $("#device-roles").val();
|
mutation.type !== 'attributes' ||
|
||||||
var value3 = $("#sites").val();
|
mutation.attributeName !== 'data-netbox-color-mode' ||
|
||||||
$.ajax({
|
!(mutation.target instanceof HTMLElement)
|
||||||
type: "POST",
|
)
|
||||||
url: "/api/plugins/topology-views/search/search/",
|
return
|
||||||
data: JSON.stringify({
|
const { netboxColorMode } = mutation.target.dataset
|
||||||
'name': value,
|
options.nodes.font.color = netboxColorMode === 'dark' ? '#fff' : '#000'
|
||||||
'devicerole': value2,
|
graph.setOptions(options)
|
||||||
'sites': value3
|
})
|
||||||
}),
|
)
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
dataType: "json",
|
|
||||||
success: function (data_result) {
|
|
||||||
$("#status").html('<span class="badge badge-pill badge-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();
|
|
||||||
|
|
||||||
graph.on('afterDrawing', function () {
|
observer.observe(document.documentElement, {
|
||||||
$("#status").html('<span class="badge badge-pill badge-success">Ready</span>');
|
attributes: true,
|
||||||
});
|
attributeFilter: ['data-netbox-color-mode']
|
||||||
|
})
|
||||||
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/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="badge badge-pill badge-warning">Failed to update coordinates</span>');
|
|
||||||
},
|
|
||||||
success: function (data_result) {
|
|
||||||
$("#coordstatus").html('<span class="badge badge-pill badge-success">Updated coordinates</span>');
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/reload_devices',
|
|
||||||
type: 'GET',
|
|
||||||
headers: { "X-CSRFToken": csrftoken },
|
|
||||||
success: function (data) {
|
|
||||||
//nothing
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: function (error_result) {
|
|
||||||
$("#status").html('<span class="badge badge-pill badge-warning">Something went wrong</span>');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.0",
|
||||||
|
"scripts": {
|
||||||
|
"bundle": "node bundle.js",
|
||||||
|
"bundle:styles": "node bundle.js --styles",
|
||||||
|
"bundle:scripts": "node bundle.js --scripts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vis-data": "^7.1.4",
|
||||||
|
"vis-network": "^9.1.2",
|
||||||
|
"vis-util": "^5.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.12.24",
|
||||||
|
"esbuild-sass-plugin": "^1.5.2",
|
||||||
|
"@egjs/hammerjs": "^2.0.0",
|
||||||
|
"component-emitter": "^1.3.0",
|
||||||
|
"keycharm": "^0.4.0",
|
||||||
|
"timsort": "^0.3.0",
|
||||||
|
"uuid": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,33 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Topology Views</h5>
|
||||||
|
<div class="controls">
|
||||||
|
<div class="control-group">
|
||||||
|
{% block extra_controls %}{% endblock %}
|
||||||
|
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
|
||||||
|
<i class="mdi mdi-download"></i>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-sm btn-danger" data-bs-dismiss="modal">
|
||||||
|
<i class="mdi mdi-close-thick"></i>
|
||||||
|
Close
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body" >
|
||||||
|
<div style="width: 500px; height: 500px;">
|
||||||
|
<div id="visgraph" style="width: 100%; height: 100%;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
const brokenImage = '{{ broken_image }}';
|
||||||
|
const topologyData = {{ topology_data | safe }};
|
||||||
|
</script>
|
||||||
|
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Topology Views Images{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content-wrapper %}
|
||||||
|
<form id="images" class="container py-4">
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-3 mb-2">
|
||||||
|
{% for role in roles %}
|
||||||
|
<div class="d-flex flex-column align-items-center">
|
||||||
|
<div class="image-dropdown dropdown">
|
||||||
|
<button class="btn btn-secondary" type="button" id="dropdownMenuButton{{ role.id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{% if role.image %}
|
||||||
|
<img src="{{ role.image }}" />
|
||||||
|
{% else %}
|
||||||
|
Image
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu mt-2" aria-labelledby="dropdownMenuButton{{ role.id }}">
|
||||||
|
<div class="image-dropdown-content">
|
||||||
|
{% for image in images %}
|
||||||
|
<img src="{{ image.url }}" title="{{ image.title }}" width="64" data-role="{{ role.id }}" data-image="{{ image.url }}"/>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>{{ role.name }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
{% include 'netbox_topology_views/toasts.html' %}
|
||||||
|
{% endblock content-wrapper %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{% static 'netbox_topology_views/js/images.js' %}" defer></script>
|
||||||
|
{% endblock javascript %}
|
||||||
@@ -1,75 +1,79 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load buttons %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load perms %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block content %}
|
{% block title %}Topology Views{% endblock %}
|
||||||
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
|
||||||
|
{% block head %}
|
||||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div class="row ">
|
{% block controls %}
|
||||||
<div class="col-md-9">
|
<div class="controls">
|
||||||
<div class="panel panel-default">
|
<div class="control-group">
|
||||||
<div class="panel-heading">
|
{% block extra_controls %}{% endblock %}
|
||||||
<strong>Network</strong> <span id="status"></span>
|
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
|
||||||
</div>
|
<i class="mdi mdi-download"></i>
|
||||||
<div class="panel-body">
|
Download
|
||||||
<div id="visgraph" class=""></div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
{% endblock controls %}
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Search</strong>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<form id="search-form" class="form">
|
|
||||||
{% 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>
|
|
||||||
<hr class="mb-4">
|
|
||||||
<button class="btn btn-primary btn-lg btn-block" type="submit" id="start-search">Search</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</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 %}
|
||||||
|
|
||||||
<div class="panel panel-default mt-3">
|
{% block content-wrapper %}
|
||||||
<div class="panel-heading">
|
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
|
||||||
<strong>Settings</strong>
|
<div class="tab-content">
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endwith %}
|
{# Applied filters #}
|
||||||
{% endblock %}
|
{% 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 %}
|
{% block javascript %}
|
||||||
|
<script type="text/javascript">
|
||||||
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
|
const brokenImage = '{{ broken_image }}';
|
||||||
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
|
const topologyData = {{ topology_data | safe }};
|
||||||
|
</script>
|
||||||
<script type="application/javascript"> iniPlotboxIndex()</script>
|
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
|
||||||
|
{% endblock javascript %}
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
|
||||||
|
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
|
||||||
|
Topology
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<div id="django-messages" class="toast-container">
|
||||||
|
<div
|
||||||
|
id="topology-plugin-error-toast"
|
||||||
|
class="toast align-items-center border-0 bg-danger hide"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
data-bs-delay="2000"
|
||||||
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
😢
|
||||||
|
<span>Error</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close me-2 m-auto"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="topology-plugin-success-toast"
|
||||||
|
class="toast align-items-center border-0 bg-success hide"
|
||||||
|
role="alert"
|
||||||
|
aria-live="assertive"
|
||||||
|
aria-atomic="true"
|
||||||
|
data-bs-delay="2000"
|
||||||
|
>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
👍
|
||||||
|
<span>Success</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close me-2 m-auto"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
from django.views.generic.base import RedirectView
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
|
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
|
||||||
# a specific view so that it can be accessed by users.
|
# a specific view so that it can be accessed by users.
|
||||||
urlpatterns = (
|
urlpatterns = (
|
||||||
path('', views.TopologyHomeView.as_view(), name='home'),
|
path("", RedirectView.as_view(url="topology/", permanent=True)),
|
||||||
|
path("topology/", views.TopologyHomeView.as_view(), name="home"),
|
||||||
|
path("images/", views.TopologyImagesView.as_view(), name="images"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,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,9 +1,769 @@
|
|||||||
from django.shortcuts import get_object_or_404, render
|
import json
|
||||||
from django.views.generic import View
|
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")
|
||||||
|
|
||||||
class TopologyHomeView(View):
|
|
||||||
"""
|
"""
|
||||||
Show the home page
|
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,20 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"name": "netbox_topology_views",
|
|
||||||
"version": "0.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-data": "^6.2.3",
|
|
||||||
"vis-util": "^1.1.8",
|
|
||||||
"vis-network": "^6.4.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(
|
setup(
|
||||||
name='netbox-topology-views',
|
name="netbox-topology-views",
|
||||||
version='0.2',
|
version="3.2.0",
|
||||||
description='An NetBox plugin to create Topology maps',
|
description="An NetBox plugin to create Topology maps",
|
||||||
url='https://github.com/mattieserver/netbox-topology-views',
|
long_description=long_description,
|
||||||
author='Mattijs Vanhaverbeke',
|
long_description_content_type="text/markdown",
|
||||||
license='Apache 2.0',
|
url="https://github.com/mattieserver/netbox-topology-views",
|
||||||
|
author="Mattijs Vanhaverbeke",
|
||||||
|
license="Apache 2.0",
|
||||||
install_requires=[],
|
install_requires=[],
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
keywords=["netbox-plugin"],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python',
|
"Programming Language :: Python",
|
||||||
'Programming Language :: Python :: 3',
|
"Programming Language :: Python :: 3",
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
],
|
],
|
||||||
project_urls={
|
project_urls={
|
||||||
'Source': 'https://github.com/mattieserver/netbox-topology-views',
|
"Source": "https://github.com/mattieserver/netbox-topology-views",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user