Compare commits

..
138 Commits
Author SHA1 Message Date
mattieserver 2e34f7435a Release v3.2.0 (#234)
* bump version

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

* build js

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

* Fixed 'Hide Unconnected' and other bugs

* rename var + extra check

* Add filter selection image for readme

* Added guidance on using the selection options

* Renaming to selection_options

* Renamed filter_selection to selection_options

* Update README.md

* Bug fix

* fix parameter order

* differentiate line type for interface connections

* allow both cables and logical connections

* Change logical connection line type

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

* Hide redundant connections, configurable options

* updated readme + removed visjs setting from settings

* Update README.md

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

* updated readme for 'Show Logical Connections'

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

* add filter field

* fixed model

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

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

* used new PluginMenu class
2022-12-21 17:03:47 +01:00
Mario 2f65614fcb Merge pull request #226 from dreng/pull_request_template
Create pull_request_template.md
2022-12-20 19:54:44 +01:00
Mario 2722dfa793 Create pull_request_template.md 2022-12-20 19:38:36 +01:00
mattieserver 41d6afc336 bump version (#222) 2022-12-18 18:50:58 +01:00
mattieserver 2901dbb5c7 add check for python3.9 (#221) 2022-12-18 18:45:49 +01:00
mattieserverandMario 37122cd76c initial update of docs (#213)
* initial update of docs

* updated docs

* Update dev_setup/README.md

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

* Update dev_setup/README.md

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

* Update dev_setup/README.md

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

* added docker / native install for psql

* Update dev_setup/README.md

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

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

* Add mdi icons to Topology and Close Buttons

* Fix buttons tag and class

* fixed drag & site filter

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

* Create feature_request.yaml

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

* [up] use bundler to bundle js

* [fix] remove domcontentloaded since defer is used

* [fix] formatting with black

* [fix] images drf api

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

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

* [fix] kebab case instead of slugification

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

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

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

* added circuit (or at least part of)

* refactor circuits + fix js

* small changes

* small changes

* added power + rework settings

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

* keeps x/y when not saving coords

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

* remove gulpfile

* cleanup unused ref

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

* open new tab with device info
2022-08-03 11:46:37 +02:00
mattieserver 1b36b9e7df added extra check (#135) 2022-08-03 10:59:02 +02:00
mattieserver 652a44437e Create SECURITY.md 2022-07-29 09:33:15 +02:00
mattieserver 76141e70ce update gh actions 2022-06-30 22:47:55 +02:00
mattieserver 76b4f6ac9a Create docker-publish.yml 2022-06-30 22:40:18 +02:00
mattieserver 953d7480af add dockerfile (#130) 2022-06-30 22:39:18 +02:00
Mattijs Vanhaverbeke f323278f00 bump version 2022-06-30 22:22:47 +02:00
mattieserver 56a6f22bf6 added site to device tooltip (#128) 2022-06-30 22:18:35 +02:00
Mattijs Vanhaverbeke b9d3cc3c4e bump version 2022-06-30 22:06:33 +02:00
Mattijs Vanhaverbeke d4833071df added filter on device status 2022-06-30 22:03:23 +02:00
Mattijs Vanhaverbeke 877d308574 small config change 2022-06-30 19:27:11 +02:00
pycolas 83b662eac3 End to end connections (#113)
* Add hide-device-role feature

* Add hide-device-role feature

* Remove end^Cevices not in queryset

* clean up

* Fix destination.device.id test

* Query optimization

* multiple updates
2022-06-30 19:13:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 53a996edbc Bump minimist from 1.2.5 to 1.2.6 (#110)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 11:38:34 +02:00
Mattijs Vanhaverbeke 398deb5382 bump version 2022-05-10 09:28:21 +02:00
Alexander Ryazanov b5991a5f50 Improve visualization performance (substantially) (#109)
* fix: moved image generation to avoid severe performance issues

* fix: moved button finding code before data handling

* fix: fixed leftover invalid variable naming
2022-05-10 09:21:22 +02:00
mattieserver de92627176 add shadow to cables (#98)
* addes shadow to edge

* changed bg color in dark mode
2022-05-06 06:43:06 +02:00
mattieserver 6be028b9dc fix typo (#100) 2022-04-15 19:42:32 +02:00
Mattijs Vanhaverbeke 8997c38263 bump version 2022-04-11 13:12:04 +02:00
Mattijs Vanhaverbeke d927e40867 bump version 2022-03-25 13:16:00 +01:00
Jeremy Stretchandmattieserver 62c0dbddb2 Initial work on NetBox v3.2 support (#88)
Co-authored-by: mattieserver <3049868+mattieserver@users.noreply.github.com>
2022-03-25 13:13:33 +01:00
mattieserver e68a8a1af0 added tenants (#94) 2022-03-25 10:12:36 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 68e8aa0512 Bump path-parse from 1.0.6 to 1.0.7 (#74)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-18 12:15:05 +01:00
mattieserver b792128415 Fix dark mode (#90)
* fixed dark mode
2022-03-10 11:46:00 +01:00
mattieserver f5d9f589bf Fix circuit error (#87)
* fix 'Interface' object has no attribute 'circuit'
2022-03-09 21:00:55 +01:00
mattieserver 3e5b28f208 Update to support netbox v3.1 (#82)
* init for v3.1

* added coords + bump version

* fix filter + added preselected roles

* added draw_default_layout

* upgraded packages

* fixed edge/node labels

* fixed enable_circuit_terminations

* added filter for unconnected devices

* added ignore_cable_type

* fixed old settings using a string instead of array

* removed distutils

* fixed coords check

* added coor saving

* added doc img

* updated docs

* bump version

* bump version
2022-03-08 17:03:47 +01:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3b4209223e Bump lodash from 4.17.20 to 4.17.21 (#66)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-12 22:13:26 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6fc7cce79a Bump hosted-git-info from 2.8.8 to 2.8.9 (#67)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-12 22:13:11 +02:00
mattieserver fd4a1cd0c7 Create codeql-analysis.yml 2021-05-07 16:20:55 +02:00
mattieserver 2deaed1adc Update README.md 2021-05-03 18:53:25 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 55a61d4d10 Bump ini from 1.3.5 to 1.3.8 (#47)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-03 18:12:42 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 3a4652543f Bump yargs-parser from 5.0.0 to 5.0.1 (#62)
Bumps [yargs-parser](https://github.com/yargs/yargs-parser) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/yargs/yargs-parser/releases)
- [Changelog](https://github.com/yargs/yargs-parser/blob/v5.0.1/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs-parser/compare/v5.0.0...v5.0.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-02 22:33:51 +02:00
mattieserver 9b4dd77943 update version 2021-05-01 19:30:18 +02:00
mattieserver 91a690f1c8 add filter by locations (#63)
* frontend for locations

* locations filter backend
2021-05-01 19:22:28 +02:00
mattieserver 5f498f9ce2 Hide unconnected (#61)
* #57 hide unconnected devices
2021-05-01 18:34:50 +02:00
mattieserver 195ac47542 added docs to setup dev (#60) 2021-05-01 17:05:32 +02:00
julianze a25be917ee Update __init__.py (#55)
fix typo toplogoy
2021-03-28 11:40:03 +02:00
mattieserver 079361b190 Update README.md 2021-02-17 11:02:58 +01:00
mattieserverandmattieserver a314d2b307 netbox v2.10 (#52)
* small changes

* removed print

* fix coords

* fix saving coords

* update readme

* fixed icons

* bump version

Co-authored-by: mattieserver <mattijs@DESKTOP-618OSU1.localdomain>
2021-02-11 19:22:41 +01:00
mattieserver 0340522f59 update jquery version to match netbox v2.9.1 (#43)
* Update full.html

* Update setup.py

* Update package.json

* Update __init__.py
2020-08-23 11:53:52 +02:00
Mattijs Vanhaverbeke 2edd597871 bump version 2020-07-20 15:49:06 +02:00
mattieserver 5f4f97c360 Merge pull request #41 from mattieserver/32
added Filter topology via URL Parameter
2020-07-20 15:00:10 +02:00
Mattijs Vanhaverbeke 17ae8b4a1d #32 2020-07-20 14:57:09 +02:00
mattieserver bb9765e011 Merge pull request #40 from mattieserver/37
add filter by region
2020-07-20 12:38:15 +02:00
Mattijs Vanhaverbeke 5bfc47775b #37 2020-07-20 12:36:38 +02:00
mattieserver 216508170f Merge pull request #39 from mattieserver/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-20 11:07:20 +02:00
dependabot[bot] 0faa135978 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-20 07:57:54 +00:00
mattieserver 9ba61cce28 Merge pull request #28 from mattieserver/22052020_dev
22052020 dev
2020-05-24 18:31:07 +02:00
mattieserver 9824a3f659 Merge branch 'master' into 22052020_dev 2020-05-24 18:30:59 +02:00
mattieserver 4115584306 Update README.md 2020-05-23 18:20:04 +02:00
mattieserver cd239ac6ae Update README.md 2020-05-23 18:19:13 +02:00
mattieserver 030844f2fb Merge pull request #27 from ryanmerolle/rm-issue-26
Update README to include local_requirements setup
2020-05-23 18:15:56 +02:00
ryanmerolle 2e6dd7de23 Update README to include local_requirements setup 2020-05-23 12:04:19 -04:00
Mattijs Vanhaverbeke de5707ff06 PATCH instead of POST 2020-05-22 12:57:58 +02:00
Mattijs Vanhaverbeke 371c5c3502 added download button, fixed perms 2020-05-22 12:07:46 +02:00
mattieserver 26139cbb1c Merge pull request #24 from mattieserver/19052020_dev_1
19052020 dev 1
2020-05-20 08:11:04 +02:00
Mattijs Vanhaverbeke 4ea315d4b2 v0.4.8 2020-05-20 08:09:45 +02:00
Mattijs Vanhaverbeke 40ae715eb5 more NoneType checks 2020-05-19 22:35:55 +02:00
Mattijs Vanhaverbeke 204098a143 #23 check for nonetype in cable.termination 2020-05-19 22:17:13 +02:00
mattieserver 9b606643ea Update README.md 2020-05-19 19:50:56 +02:00
mattieserver f184bdf2f3 Merge pull request #20 from mattieserver/19052020_dev
#14, added circuit terminations
2020-05-19 17:20:11 +02:00
Mattijs Vanhaverbeke 370eff6b67 #14, added circuit terminations 2020-05-19 17:18:20 +02:00
Mattijs Vanhaverbeke 6d28ce2da4 v0.4.6 2020-05-18 19:12:32 +02:00
mattieserver 8a97923bb2 Merge pull request #19 from JohannesKreuzer/color-edges
color the edges
2020-05-18 19:10:10 +02:00
Johannes Kreuzer 7f0dcd97f7 color the edges
When a cable has a color set, use this color for the edge which
repesents the cable, so that a cabling type or maybe a purpose of the cable
could be seen in the topology graph.
2020-05-18 19:03:18 +02:00
Mattijs Vanhaverbeke 0d7052d83d small cleanup and v0.4.5 2020-05-18 18:52:56 +02:00
mattieserver e50945f8b5 Merge pull request #18 from JohannesKreuzer/add-dev-details
Shorter Label and more details for device
2020-05-18 18:19:57 +02:00
Johannes Kreuzer 987aab772f move device type name to title and add more device details to title 2020-05-18 18:14:23 +02:00
Mattijs Vanhaverbeke 2ef6e7cb96 v0.4.4 2020-05-17 17:08:38 +02:00
mattieserver 570d241b7f Merge pull request #17 from mattieserver/17052020_dev_3
fixed spelling of the word 'unknown'
2020-05-17 17:06:47 +02:00
Mattijs Vanhaverbeke 2025bba6a1 fixed spelling of the word 'unknown' 2020-05-17 17:05:14 +02:00
mattieserver 22ec4fb310 Merge pull request #16 from mattieserver/17052020_dev_2
17052020 dev 2
2020-05-17 16:46:36 +02:00
Mattijs Vanhaverbeke 85b27895e2 v0.4.3 2020-05-17 16:45:19 +02:00
Mattijs Vanhaverbeke 940b37428c #15 check for NoneType in cables & devices 2020-05-17 16:41:51 +02:00
mattieserver eedcf65bd8 Update README.md 2020-05-17 14:59:55 +02:00
mattieserver bf28d2d2d1 Update README.md 2020-05-17 14:59:03 +02:00
mattieserver a57e1f79ab Merge pull request #13 from mattieserver/17052020_dev_1
17052020 dev 1
2020-05-17 14:37:47 +02:00
Mattijs Vanhaverbeke 6e142bdb74 v0.4.2 2020-05-17 14:37:05 +02:00
Mattijs Vanhaverbeke 79cb8d8e2e fixes searhc (#12) 2020-05-17 14:35:23 +02:00
mattieserver 1c9d4d8a1e Merge pull request #11 from mattieserver/17052020_dev
17052020 dev
2020-05-17 14:22:34 +02:00
Mattijs Vanhaverbeke 6fc32b9bfa v0.4.1 2020-05-17 14:20:30 +02:00
Mattijs Vanhaverbeke e0624effc2 exclude "circuit termination" type for now 2020-05-17 14:14:53 +02:00
mattieserver fe94765c15 Merge pull request #9 from mattieserver/14052020_dev
14052020 dev
2020-05-16 20:03:10 +02:00
Mattijs Vanhaverbeke ffd91e2cf5 CustomFieldValue & CustomField 2020-05-16 20:00:25 +02:00
Mattijs Vanhaverbeke 44e82c0286 updated vis-network and fixed dragevent update 2020-05-16 15:48:52 +02:00
Mattijs Vanhaverbeke 33d7157b12 added search by tag (#8) 2020-05-16 14:24:38 +02:00
mattieserver 5c4c6be745 Create LICENSE 2020-05-14 22:32:14 +02:00
Mattijs Vanhaverbeke 5e53066ba7 fixed auth, added save-coords 2020-05-14 22:20:01 +02:00
mattieserver a1d155fdbc Merge pull request #7 from mattieserver/dev
bump version to 0.3
2020-05-14 18:26:48 +02:00
mattijs vanhaverbeke ab944fdcef bump version to 0.3 2020-05-14 18:19:16 +02:00
mattieserver f72ca57ac7 Merge pull request #5 from JohannesKreuzer/netbox-subdir-support
support of netbox deployments in subdirectories
2020-05-14 18:17:23 +02:00
Johannes Kreuzer 8a6f1660f0 Update for support of netbox deployments, which are not in the webserver root (e.g. /netbox/) 2020-05-14 15:52:47 +02:00
mattieserver d608d63207 Update README.md 2020-04-24 12:57:47 +02:00
mattieserver 4d161fe6ac Update README.md 2020-04-24 12:56:47 +02:00
mattieserver c6a6893c10 Update README.md 2020-04-24 12:45:49 +02:00
mattieserver b2430d1723 Update README.md 2020-04-17 11:13:10 +02:00
61 changed files with 3915 additions and 4598 deletions
+52
View File
@@ -0,0 +1,52 @@
---
name: 🐛 Bug Report
description: Report a reproducible bug in the current release of NetBox
labels: ["type: bug"]
body:
- type: markdown
attributes:
value: >
**NOTE:** This form is only for reporting _reproducible bugs_. If you're having
trouble with installation or just looking for assistance with using NetBox
Topology Views, please visit our
[discussion forum](https://github.com/mattieserver/netbox-topology-views/discussions) instead.
- type: input
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.9
validations:
required: true
- type: input
attributes:
label: Topology Views version
description: What version of NetBox Topology Views are you currently running?
placeholder: v3.1.0
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: >
Describe in detail the exact steps that someone else can take to
reproduce this bug using the current stable release of NetBox Topology Views.
placeholder: |
1. Click on "create widget"
2. Set foo to 12 and bar to G
3. Click the "create" button
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: A new widget should have been created with the specified attributes
validations:
required: true
- type: textarea
attributes:
label: Observed Behavior
description: What happened instead?
placeholder: A TypeError exception was raised
validations:
required: true
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ❓ Discussion
url: https://github.com/mattieserver/netbox-topology-views/discussions
about: "If you're just looking for help, try starting a discussion instead"
@@ -0,0 +1,58 @@
---
name: ✨ Feature Request
description: Propose a new NetBox Topology Views feature or enhancement
labels: ["type: feature"]
body:
- type: markdown
attributes:
value: >
**NOTE:** This form is only for submitting well-formed proposals to extend or modify
NetBox Topology Views in some way. If you're trying to solve a problem but can't figure
out how, or if you still need time to work on the details of a proposed new feature,
please start a [discussion](https://github.com/mattieserver/netbox-topology-views/discussions) instead.
- type: input
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.3.9
validations:
required: true
- type: input
attributes:
label: Topology Views version
description: What version of NetBox Topology Views are you currently running?
placeholder: v3.1.0
validations:
required: true
- type: dropdown
attributes:
label: Feature type
options:
- New functionality
- Change to existing functionality
validations:
required: true
- type: textarea
attributes:
label: Proposed functionality
description: >
Describe in detail the new feature or behavior you are proposing. Include any specific changes
to work flows and/or the user interface. The more detail you provide here, the
greater chance your proposal has of being discussed. Feature requests which don't include an
actionable implementation plan will be rejected.
validations:
required: true
- type: textarea
attributes:
label: Use case
description: >
Explain how adding this functionality would benefit NetBox Topology Views users. What need does it address?
validations:
required: true
- type: textarea
attributes:
label: External dependencies
description: >
List any new dependencies on external libraries or services that this new feature would
introduce. For example, does the proposal require the installation of a new Python package?
(Not all new features introduce new dependencies.)
+21
View File
@@ -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.
-->
+71
View File
@@ -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
+11 -11
View File
@@ -5,7 +5,7 @@ name: Upload Python Package
on:
release:
types: [created]
types: [published]
jobs:
deploy:
@@ -13,19 +13,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
+5 -1
View File
@@ -110,6 +110,9 @@ ENV/
env.bak/
venv.bak/
# VS Code / VS Codium project settings
.vscode/
# Spyder project settings
.spyderproject
.spyproject
@@ -128,4 +131,5 @@ dmypy.json
# Pyre type checker
.pyre/
node_modules
node_modules
app.js.map
+201
View File
@@ -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.
+148 -10
View File
@@ -1,17 +1,155 @@
# netbox-topology-views
# Netbox Topology Views Plugin
## preview
![Version](https://img.shields.io/pypi/v/netbox-topology-views) ![Downloads](https://img.shields.io/pypi/dm/netbox-topology-views)
![preview image](doc/img/preview.png?raw=true "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
![preview image](doc/img/preview_3.1.jpeg?raw=true "preview")
There is also support for custom fiels.
If you create a custom field "coordinates" for "dcim > device" with type "text" and name "coordinates" you will see the same layout every time.
## Install
## 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:
![preview image](doc/img/selection_options.png?raw=true "preview")
<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
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| > 2.0 | :white_check_mark: |
| < 2.0 | :x: |
## Reporting a Vulnerability
Please create an issue when you encounter a Security issue.
+143
View File
@@ -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.
+40
View File
@@ -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'
+41
View File
@@ -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
View File
@@ -1,78 +0,0 @@
var gulp = require('gulp');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var cleanCss = require('gulp-clean-css');
var jsVendorFiles = [
'node_modules/vis-network/standalone/umd/vis-network.min.js',
];
var cssVendorFiles = [
'node_modules/vis-network/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);
+28 -10
View File
@@ -1,18 +1,36 @@
from extras.plugins import PluginConfig
class TopologyViewsConfig(PluginConfig):
name = 'netbox_topology_views'
verbose_name = 'Topology views'
description = 'An plugin to render toplogoy maps'
version = '0.2'
author = 'Mattijs Vanhaverbeke'
author_email = 'author@example.com'
base_url = 'topology-views'
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "3.2.0"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
required_settings = []
default_settings = {
'preselected_device_roles': 'Firewall,Router,Distribution Switch,Core Switch,Internal Switch,Access Switch,Server,Storage,Backup,Wireless AP',
'IGNORE_CABLE_TYPE': 'dcim.poweroutlet,dcim.powerport',
'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'
"preselected_device_roles": [
"Firewall",
"Router",
"Distribution Switch",
"Core Switch",
"Internal Switch",
"Access Switch",
"Server",
"Storage",
"Backup",
"Wireless AP",
],
"ignore_cable_type": [],
"static_image_directory": "netbox_topology_views/img",
"allow_coordinates_saving": False,
"always_save_coordinates": False,
"preselected_tags": [],
"draw_default_layout": False,
"hide_single_cable_logical_conns": False,
}
config = TopologyViewsConfig
-3
View File
@@ -1,3 +0,0 @@
from django.contrib import admin
#no models yet
+1
View File
@@ -0,0 +1 @@
"""REST API"""
+16 -4
View File
@@ -1,10 +1,22 @@
from dcim.models import Device, DeviceRole
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:
model = DeviceRole
fields = ('id', 'name')
fields = ("name", "slug", "color", "vm_role", "description")
+7 -6
View File
@@ -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.register('preselectdeviceroles', PreSelectDeviceRolesViewSet)
router.register('search', SearchViewSet, basename='search')
router = NetBoxRouter()
urlpatterns = router.urls
router.register("save-coords", views.SaveCoordsViewSet)
router.register("images", views.SaveRoleImageViewSet)
urlpatterns = router.urls
+115 -76
View File
@@ -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.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet, ViewSet
from .serializers import PreDeviceRoleSerializer
from django.conf import settings
from utilities.api import IsAuthenticatedOrLoginNotRequired
from dcim.models import DeviceRole, Device, Cable
from netbox_topology_views.api.serializers import (
RoleImageSerializer,
TopologyDummySerializer,
)
from netbox_topology_views.models import RoleImage
from netbox_topology_views.utils import get_image_from_url
class PreSelectDeviceRolesViewSet(ReadOnlyModelViewSet):
preselected_device_roles_raw = settings.PLUGINS_CONFIG["netbox_topology_views"]["preselected_device_roles"]
preselected_device_roles = preselected_device_roles_raw.split(",")
queryset = DeviceRole.objects.filter(name__in=preselected_device_roles)
serializer_class = PreDeviceRoleSerializer
class SaveCoordsViewSet(ReadOnlyModelViewSet):
queryset = Device.objects.none()
serializer_class = TopologyDummySerializer
class SearchViewSet(ViewSet):
_ignore_model_permissions = True
permission_classes = [IsAuthenticatedOrLoginNotRequired]
@action(detail=False, methods=["patch"])
def save_coords(self, request):
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):
filter_devices = Device.objects.all()
if name is not None:
filter_devices = filter_devices.filter(name__contains=name)
if site is not None:
filter_devices = filter_devices.filter(site__id__in=site)
if role is not None:
filter_devices = filter_devices.filter(device_role__id__in=role)
return filter_devices
device_id: str = request.data.get("node_id", None)
x_coord = request.data.get("x", None)
y_coord = request.data.get("y", None)
actual_device = None
if device_id.startswith("c"):
device_id = device_id.lstrip("c")
actual_device = Circuit.objects.get(id=device_id)
elif device_id.startswith("p"):
device_id = device_id.lstrip("p")
actual_device = PowerPanel.objects.get(id=device_id)
elif device_id.startswith("f"):
device_id = device_id.lstrip("f")
actual_device = PowerFeed.objects.get(id=device_id)
elif device_id.isnumeric():
actual_device = Device.objects.get(id=device_id)
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'])
def search(self, request):
name = None
sites = None
devicerole = None
if "devicerole" in request.data:
if request.data["devicerole"]:
devicerole = request.data["devicerole"]
if "sites" in request.data:
if request.data["sites"]:
sites = request.data["sites"]
if "name" in request.data:
if request.data["name"]:
name = request.data["name"]
class SaveRoleImageViewSet(PermissionRequiredMixin, ViewSet):
queryset = DeviceRole.objects.none()
serializer_class = RoleImageSerializer
permission_required = (
"dcim.add_device_role",
"dcim.change_device_role",
)
devices = self._filter(sites, devicerole, name)
nodes = []
edges = []
edge_ids = 0
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"
def create(self, request):
if not isinstance(request.data, dict):
return JsonResponse(
{"status": "Missing or malformed request body"}, status=400
)
for device_custom_field in device.custom_field_values.all():
if device_custom_field.field.name == "coordinates":
cords = device_custom_field.serialized_value.split(";")
node["x"] = int(cords[0])
node["y"] = int(cords[1])
node["physics"] = False
nodes.append(node)
device_roles = {k: v for k, v in request.data.items() if k.isnumeric()}
content_type_ids = {
k[2:]: v
for k, v in request.data.items()
if k.startswith("ct") and k[2:].isnumeric()
}
results = {}
results["nodes"] = nodes
results["edges"] = edges
roles: Dict[int, DeviceRole] = DeviceRole.objects.in_bulk(device_roles.keys())
content_types: Dict[int, ContentType] = ContentType.objects.in_bulk(
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"})
+55
View File
@@ -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)
+133
View File
@@ -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'),
),
]
+92 -2
View File
@@ -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}")
+7 -8
View File
@@ -1,10 +1,9 @@
from extras.plugins import PluginMenuButton, PluginMenuItem
from utilities.choices import ButtonColorChoices
from extras.plugins import PluginMenu, PluginMenuItem
menu_items = (
PluginMenuItem(
link='plugins:netbox_topology_views:home',
link_text='Topology',
buttons=()
menu = PluginMenu(
label='Topology Views',
groups=(
('Topology', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
('CUSTOMIZATION', (PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Topology Images"),),),
),
)
)
@@ -1 +1 @@
#visgraph{height:70vh}
#visgraph{height:64vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
(()=>{var l=(e,t,o)=>new Promise((n,r)=>{var c=s=>{try{i(o.next(s))}catch(a){r(a)}},m=s=>{try{i(o.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,m);i((o=o.apply(e,t)).next())});var d=e=>{if(!document.cookie)return;let t=null,o=document.cookie.split(";");for(let n=0;n<o.length;n++){let r=o[n].trim();if(r.substring(0,e.length+1)===e+"="){t=decodeURIComponent(r.substring(e.length+1));break}}return t};var u={success:e=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()},error:e=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()}};var g={},p=d("csrftoken");document.querySelector("form#images").addEventListener("submit",e=>l(void 0,null,function*(){e.preventDefault();try{let t=yield fetch("/api/plugins/netbox_topology_views/images/",{method:"POST",body:JSON.stringify(g),headers:{"X-CSRFToken":p,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(e=>{e.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:o,image:n}}=t.currentTarget;g[o]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${o}`);r&&(r.innerHTML=`<img src="${n}" />`)})});})();
@@ -0,0 +1,6 @@
{
"version": 3,
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
"mappings": "mNAAO,GAAM,GAAY,AAAC,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,GAAI,GAAc,KACZ,EAAU,SAAS,OAAO,MAAM,KAEtC,OAAS,GAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,GAAM,GAAS,EAAQ,GAAG,OAE1B,GAAI,EAAO,UAAU,EAAG,EAAK,OAAS,KAAO,EAAO,IAAK,CACrD,EAAc,mBAAmB,EAAO,UAAU,EAAK,OAAS,IAChE,OAIR,MAAO,ICfJ,GAAM,GAAQ,CACjB,QAAS,AAAC,GAAY,CAClB,GAAM,GAAK,SAAS,cAAc,kCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,QAEV,MAAO,AAAC,GAAY,CAChB,GAAM,GAAK,SAAS,cAAc,gCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,SCZd,GAAM,GAAU,GACV,EAAY,EAAU,aAE5B,SAAS,cAAc,eAAe,iBAAiB,SAAU,AAAO,GAAM,0BAC1E,EAAE,iBACF,GAAI,CACA,GAAM,GAAM,KAAM,OAAM,6CAA8C,CAClE,OAAQ,OACR,KAAM,KAAK,UAAU,GACrB,QAAS,CACL,cAAe,EACf,eAAgB,sBAIxB,GAAI,CAAC,EAAI,GAAI,KAAM,IAAI,OAAM,KAAM,GAAI,QACvC,EAAM,QAAQ,wBACT,EAAP,CACE,QAAQ,IAAI,GACZ,EAAM,MAAM,EAAI,aAIxB,SAAS,iBAAiB,kCAAkC,QAAQ,AAAC,GAAO,CACxE,EAAG,iBAAiB,QAAS,AAAC,GAAM,CA3BxC,MA4BQ,GAAI,CAAE,GAAE,wBAAyB,cAAc,OAC/C,GAAM,CACF,QAAS,CAAE,OAAM,UACjB,EAAE,cAEN,EAAQ,GAAQ,EAEhB,GAAM,GAAS,KAAE,cACZ,QAAQ,eADE,cAET,cAAc,sBAAsB,KAC1C,AAAI,GAAQ,GAAO,UAAY,aAAa",
"names": []
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
const esbuild = require('esbuild')
const { sassPlugin } = require('esbuild-sass-plugin')
const options = {
bundle: true,
minify: true,
sourcemap: 'external',
sourcesContent: false,
logLevel: 'error'
}
const ARGS = process.argv.slice(2)
const noCache = ARGS.includes('--no-cache')
async function bundleScripts() {
const entryPoints = {
app: 'js/home.js',
images: 'js/images.js'
}
try {
const result = await esbuild.build({
...options,
outdir: '../static/netbox_topology_views/js/',
entryPoints,
target: 'es2016'
})
if (result.errors.length !== 0) return
for (const [targetName, sourceName] of Object.entries(entryPoints)) {
const source = sourceName.split('/').pop() // take last element
console.log(
`✅ Bundled source file '${source}' to '${targetName}.js'`
)
}
} catch (err) {
console.error(err)
}
}
async function bundleStyles() {
try {
const entryPoints = {
vendor: 'css/_external.scss',
app: 'css/app.scss'
}
const pluginOptions = { outputStyle: 'compressed' }
// Allow cache disabling.
if (noCache) {
pluginOptions.cache = false
}
const result = await esbuild.build({
...options,
outdir: '../static/netbox_topology_views/css/',
// Disable sourcemaps for CSS/SCSS files, see #7068
sourcemap: false,
entryPoints,
plugins: [sassPlugin(pluginOptions)],
loader: {
'.eot': 'file',
'.woff': 'file',
'.woff2': 'file',
'.svg': 'file',
'.ttf': 'file'
}
})
if (result.errors.length === 0) {
for (const [targetName, sourceName] of Object.entries(
entryPoints
)) {
const source = sourceName.split('/')[1]
console.log(
`✅ Bundled source file '${source}' to '${targetName}.css'`
)
}
}
} catch (err) {
console.error(err)
}
}
async function bundleAll() {
if (ARGS.includes('--styles')) {
// Only run style jobs.
return await bundleStyles()
}
if (ARGS.includes('--scripts')) {
// Only run script jobs.
return await bundleScripts()
}
await bundleStyles()
await bundleScripts()
}
bundleAll()
@@ -0,0 +1 @@
@import '../node_modules/vis-network/dist/dist/vis-network.min.css';
+25 -1
View File
@@ -1,3 +1,27 @@
#visgraph {
height: 70vh;
height: 64vh;
}
html[data-netbox-color-mode=dark] #visgraph {
background-color: #212529;
}
.image-dropdown img {
width: 64px;
height: 64px;
}
.image-dropdown-content {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding-inline: 0.5rem;
width: 50vw;
max-width: 32rem;
> img {
cursor: pointer;
}
}
@@ -0,0 +1,17 @@
export const getCookie = (name) => {
if (!document.cookie) return
let cookieValue = null
const cookies = document.cookie.split(';')
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + '=') {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1))
break
}
}
return cookieValue
}
+126 -165
View File
@@ -1,9 +1,8 @@
var graph = null;
var container = null;
var csrftoken = null;
var nodes = new vis.DataSet();
var edges = new vis.DataSet();
var options = {
import { DataSet } from 'vis-data/esnext'
import { Network } from 'vis-network/esnext'
import { getCookie } from './csrftoken.js'
const options = {
interaction: {
hover: true,
hoverConnectedEdges: true,
@@ -11,184 +10,146 @@ var options = {
},
nodes: {
shape: 'image',
brokenImage: '/static/netbox_topology_views/img/role-unknown.png',
brokenImage: brokenImage ?? '',
size: 35,
font: {
multi: 'md',
face: 'helvetica',
},
color:
document.documentElement.dataset.netboxColorMode === 'dark'
? '#fff'
: '#000'
}
},
edges: {
length: 100,
width: 2,
font: {
face: 'helvetica',
face: 'helvetica'
},
shadow: {
enabled: true
}
},
physics: {
solver: 'forceAtlas2Based'
}
};
function iniPlotboxIndex() {
document.addEventListener('DOMContentLoaded', function () {
container = document.getElementById('visgraph');
csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
startLoadSearchBar();
handleButtonPress();
}, false);
}
function startLoadSearchBar() {
$('#device-roles').select2({
allowClear: true,
placeholder: "---------",
theme: "bootstrap",
multiple: true,
ajax: {
url: "/api/dcim/device-roles/?brief=true",
dataType: "json",
type: "GET",
data: function (params) {
var queryParameters = {
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) {
// Load CSRF token
const csrftoken = getCookie('csrftoken')
var queryParameters = {
term: params.term
}
return queryParameters;
},
processResults: function (data) {
return {
results: $.map(data.results, function (item) {
return {
text: item.name,
id: item.id
}
})
};
}
}
});
// Render vis graph
let graph = null // vis graph instance
var deviceRolesSelect = $('#device-roles');
$.ajax({
type: 'GET',
url: '/api/plugins/topology-views/preselectdeviceroles/'
}).then(function (data) {
$.each(data.results, function (index, device_role_to_preload) {
var option = new Option(device_role_to_preload.name, device_role_to_preload.id, true, true);
deviceRolesSelect.append(option).trigger('change');
deviceRolesSelect.trigger({
type: 'select2:select',
params: {
data: device_role_to_preload
const container = document.querySelector('#visgraph')
const coordSaveCheckbox = document.querySelector('#id_save_coords')
;(function handleLoadData() {
if (!topologyData) return
function htmlTitle(text) {
const container = document.createElement('div')
container.innerHTML = text
return container
}
const nodes = new DataSet(
topologyData.nodes.map((node) => ({
...node,
title: htmlTitle(node.title)
}))
)
const edges = new DataSet(
topologyData.edges.map((node) => ({
...node,
title: htmlTitle(node.title)
}))
)
graph = new Network(container, { nodes, edges }, options)
graph.fit()
graph.on('dragEnd', (params) => {
if (coordSaveCheckbox == null) return
if (!coordSaveCheckbox.checked) return
Promise.allSettled(
Object.entries(graph.getPositions(params.nodes)).map(
async ([nodeId, nodePosition]) => {
const res = await fetch(
'/api/plugins/netbox_topology_views/save-coords/save_coords/',
{
method: 'PATCH',
headers: {
'X-CSRFToken': csrftoken,
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
node_id: nodeId,
x: nodePosition.x,
y: nodePosition.y
})
}
)
console.log(nodeId, res.status, res.statusText)
}
});
});
});
)
)
})
graph.on('doubleClick', (params) => {
if (params.nodes.length > 0) {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
}
else {
params.edges.forEach((edge) => {
window.open(edges.get(edge).href, '_blank')
})
}
})
})()
// Download Graph
const MIME_TYPE = 'image/png'
const downloadButton = document.querySelector('#btnDownloadImage')
downloadButton.addEventListener('click', (e) => {
performGraphDownload()
})
function performGraphDownload() {
const canvas = container.querySelector('canvas')
const tempDownloadLink = document.createElement('a')
const generatedImageUrl = canvas.toDataURL(MIME_TYPE)
tempDownloadLink.href = generatedImageUrl
tempDownloadLink.download = 'topology'
document.body.appendChild(tempDownloadLink)
tempDownloadLink.click()
document.body.removeChild(tempDownloadLink)
}
function handleButtonPress() {
$("#search-form").submit(function (event) {
$("#status").html('<span class="badge badge-pill badge-info">Loading data</span>');
event.preventDefault();
var value = $("#name").val();
var value2 = $("#device-roles").val();
var value3 = $("#sites").val();
$.ajax({
type: "POST",
url: "/api/plugins/topology-views/search/search/",
data: JSON.stringify({
'name': value,
'devicerole': value2,
'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();
// Theme switching
const observer = new MutationObserver((mutations) =>
mutations.forEach((mutation) => {
if (
!graph ||
mutation.type !== 'attributes' ||
mutation.attributeName !== 'data-netbox-color-mode' ||
!(mutation.target instanceof HTMLElement)
)
return
const { netboxColorMode } = mutation.target.dataset
options.nodes.font.color = netboxColorMode === 'dark' ? '#fff' : '#000'
graph.setOptions(options)
})
)
graph.on('afterDrawing', function () {
$("#status").html('<span class="badge badge-pill badge-success">Ready</span>');
});
graph.on("dragEnd", function (params) {
dragged = this.getPositions(params.nodes);
$.each(dragged, function (node_id, coordinates) {
$("#coordstatus").html('');
if ($('#checkSaveCoordinates').is(":checked")) {
nodes.update({ id: node_id, physics: false });
$.ajax({
url: "/api/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>');
},
});
});
}
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-netbox-color-mode']
})
@@ -0,0 +1,41 @@
import { getCookie } from './csrftoken.js'
import { toast } from './toast.js'
const mapping = {}
const csrftoken = getCookie('csrftoken')
document.querySelector('form#images').addEventListener('submit', async (e) => {
e.preventDefault()
try {
const res = await fetch('/api/plugins/netbox_topology_views/images/', {
method: 'POST',
body: JSON.stringify(mapping),
headers: {
'X-CSRFToken': csrftoken,
'Content-Type': 'application/json'
}
})
if (!res.ok) throw new Error(await res.text())
toast.success('Saved settings')
} catch (err) {
console.dir(err)
toast.error(err.message)
}
})
document.querySelectorAll('form#images .dropdown-menu img').forEach((el) => {
el.addEventListener('click', (e) => {
if (!(e.currentTarget instanceof HTMLElement)) return
const {
dataset: { role, image }
} = e.currentTarget
mapping[role] = image
const button = e.currentTarget
.closest('.dropdown')
?.querySelector(`#dropdownMenuButton${role}`)
if (button) button.innerHTML = `<img src="${image}" />`
})
})
@@ -0,0 +1,18 @@
export const toast = {
success: (message) => {
const el = document.querySelector('#topology-plugin-success-toast')
if (!el) return console.error('Could not find toast component!')
const content = el.querySelector('span')
content.textContent = message
const toast = new window.Toast(el)
toast.show()
},
error: (message) => {
const el = document.querySelector('#topology-plugin-error-toast')
if (!el) return console.error('Could not find toast component!')
const content = el.querySelector('span')
content.textContent = message
const toast = new window.Toast(el)
toast.show()
}
}
+791
View File
@@ -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"
}
}
+251
View File
@@ -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==
+15
View File
@@ -0,0 +1,15 @@
from extras.plugins import PluginTemplateExtension
from django.conf import settings
from packaging import version
NETBOX_CURRENT_VERSION = version.parse(settings.VERSION)
class SiteButtons(PluginTemplateExtension):
model = 'dcim.site'
def buttons(self):
return self.render('netbox_topology_views/site_button.html')
template_extensions = [SiteButtons]
@@ -0,0 +1,33 @@
{% load static %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
<div class="modal-header">
<h5 class="modal-title">Topology Views</h5>
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<i class="mdi mdi-download"></i>
Download
</a>
<a class="btn btn-sm btn-danger" data-bs-dismiss="modal">
<i class="mdi mdi-close-thick"></i>
Close
</a>
</div>
</div>
</div>
<div class="panel-body" >
<div style="width: 500px; height: 500px;">
<div id="visgraph" style="width: 100%; height: 100%;">
</div>
</div>
</div>
<script type="text/javascript">
const brokenImage = '{{ broken_image }}';
const topologyData = {{ topology_data | safe }};
</script>
<script src="{% static 'netbox_topology_views/js/app.js' %}?ver={{ epoch }} " defer></script>
@@ -0,0 +1,46 @@
{% extends 'base/layout.html' %}
{% load buttons %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% load static %}
{% block title %}Topology Views Images{% endblock %}
{% block head %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
{% endblock %}
{% block content-wrapper %}
<form id="images" class="container py-4">
<div class="d-flex flex-wrap justify-content-center gap-3 mb-2">
{% for role in roles %}
<div class="d-flex flex-column align-items-center">
<div class="image-dropdown dropdown">
<button class="btn btn-secondary" type="button" id="dropdownMenuButton{{ role.id }}" data-bs-toggle="dropdown" aria-expanded="false">
{% if role.image %}
<img src="{{ role.image }}" />
{% else %}
Image
{% endif %}
</button>
<div class="dropdown-menu mt-2" aria-labelledby="dropdownMenuButton{{ role.id }}">
<div class="image-dropdown-content">
{% for image in images %}
<img src="{{ image.url }}" title="{{ image.title }}" width="64" data-role="{{ role.id }}" data-image="{{ image.url }}"/>
{% endfor %}
</div>
</div>
</div>
<span>{{ role.name }}</span>
</div>
{% endfor %}
</div>
<button class="btn btn-primary" type="submit">Save</button>
</form>
{% include 'netbox_topology_views/toasts.html' %}
{% endblock content-wrapper %}
{% block javascript %}
<script src="{% static 'netbox_topology_views/js/images.js' %}" defer></script>
{% endblock javascript %}
@@ -1,75 +1,79 @@
{% extends 'base.html' %}
{% extends 'base/layout.html' %}
{% load buttons %}
{% load render_table from django_tables2 %}
{% load static %}
{% load perms %}
{% load helpers %}
{% block content %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
{% block title %}Topology Views{% endblock %}
{% block head %}
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/vendor.css' %}">
<link rel="stylesheet" href="{% static 'netbox_topology_views/css/app.css' %}">
{% endblock %}
<div class="row ">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Network</strong> <span id="status"></span>
</div>
<div class="panel-body">
<div id="visgraph" class=""></div>
</div>
</div>
{% block controls %}
<div class="controls">
<div class="control-group">
{% block extra_controls %}{% endblock %}
<a id="btnDownloadImage" class="btn btn-sm btn-info" href="#">
<i class="mdi mdi-download"></i>
Download
</a>
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Search</strong>
</div>
<div class="panel-body">
<form id="search-form" class="form">
{% 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>
{% endblock controls %}
</div>
{% block tabs %}
<ul class="nav nav-tabs px-3">
{% block tab_items %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="network-tab" data-bs-toggle="tab" data-bs-target="#networks" type="button" role="tab" aria-controls="filters-form" aria-selected="true">
Network
</button>
</li>
{% if filter_form %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form" type="button" role="tab" aria-controls="object-list" aria-selected="false">
Filters
{% if filter_form %}{% badge filter_form.changed_data|length bg_color="blue" %}{% endif %}
</button>
</li>
{% endif %}
{% endblock tab_items %}
</ul>
{% endblock tabs %}
<div class="panel panel-default mt-3">
<div class="panel-heading">
<strong>Settings</strong>
</div>
<div class="panel-body">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="checkSaveCoordinates">
<label class="custom-control-label" for="checkSaveCoordinates">Save coordinates</label>
</div>
<div>
<span id="coordstatus"></span>
</div>
</div>
</div>
</div>
</div>
{% block content-wrapper %}
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content">
{% endwith %}
{% endblock %}
{# Applied filters #}
{% if filter_form %}
{% applied_filters model filter_form request.GET %}
<hr style="margin-top: 0px;">
{% endif %}
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
<div class="panel-body">
<div id="visgraph"></div>
</div>
</div>
{% if filter_form %}
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div>
{% endif %}
</div>
{% endwith %}
{% endblock content-wrapper %}
{% block javascript %}
<script src="{% static 'netbox_topology_views/js/vendor.js' %}"></script>
<script src="{% static 'netbox_topology_views/js/app.js' %}"></script>
<script type="application/javascript"> iniPlotboxIndex()</script>
{% endblock %}
<script type="text/javascript">
const brokenImage = '{{ broken_image }}';
const topologyData = {{ topology_data | safe }};
</script>
<script src="{% static 'netbox_topology_views/js/app.js' %}" defer></script>
{% endblock javascript %}
@@ -0,0 +1,4 @@
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
Topology
</a>
@@ -0,0 +1,44 @@
<div id="django-messages" class="toast-container">
<div
id="topology-plugin-error-toast"
class="toast align-items-center border-0 bg-danger hide"
role="alert"
aria-live="assertive"
aria-atomic="true"
data-bs-delay="2000"
>
<div class="d-flex">
<div class="toast-body">
😢
<span>Error</span>
</div>
<button
type="button"
class="btn-close me-2 m-auto"
data-bs-dismiss="toast"
aria-label="Close"
></button>
</div>
</div>
<div
id="topology-plugin-success-toast"
class="toast align-items-center border-0 bg-success hide"
role="alert"
aria-live="assertive"
aria-atomic="true"
data-bs-delay="2000"
>
<div class="d-flex">
<div class="toast-body">
👍
<span>Success</span>
</div>
<button
type="button"
class="btn-close me-2 m-auto"
data-bs-dismiss="toast"
aria-label="Close"
></button>
</div>
</div>
</div>
+4 -2
View File
@@ -1,10 +1,12 @@
from django.urls import path
from django.views.generic.base import RedirectView
from . import views
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
# a specific view so that it can be accessed by users.
urlpatterns = (
path('', views.TopologyHomeView.as_view(), name='home'),
path("", RedirectView.as_view(url="topology/", permanent=True)),
path("topology/", views.TopologyHomeView.as_view(), name="home"),
path("images/", views.TopologyImagesView.as_view(), name="images"),
)
+105
View File
@@ -0,0 +1,105 @@
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import Type
import sys
from django.conf import settings
from django.db.models import Model
from django.templatetags.static import static
from django.utils.text import camel_case_to_spaces, re_camel_case
IMAGE_DIR = Path(settings.STATIC_ROOT) / "netbox_topology_views/img"
if sys.version_info >= (3,9,0):
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / settings.PLUGINS_CONFIG[
"netbox_topology_views"
]["static_image_directory"].removeprefix("/")
else:
prefix = "/"
plugin_path = settings.PLUGINS_CONFIG["netbox_topology_views"]["static_image_directory"]
if plugin_path.startswith(prefix):
plugin_path_new = plugin_path[len(prefix):]
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / plugin_path_new
else:
CONF_IMAGE_DIR: Path = Path(settings.STATIC_ROOT) / plugin_path
def image_static_url(path: Path) -> str:
return settings.BASE_PATH + static(
f"/{path.relative_to(Path(settings.STATIC_ROOT))}"
)
def get_image_from_url(url: str) -> str:
url_path = settings.BASE_PATH + settings.STATIC_URL
if sys.version_info >= (3,9,0):
return url.removeprefix(url_path)
else:
if url.startswith(url_path):
url_new = url[len(url_path):]
return url_new
else:
return url
IMAGE_FILETYPES = (
"apng",
"avif",
"bmp",
"cur",
"gif",
"ico",
"jfif",
"jpeg",
"jpg",
"pjp",
"pjpeg",
"png",
"svg",
"webp",
)
def find_image_in_dir(glob: str, dir: Path):
return next(
(f for f in dir.glob(f"{glob}.*") if f.suffix.lstrip(".") in IMAGE_FILETYPES),
None,
)
@lru_cache(maxsize=50)
def find_image_url(glob: str, dir: Path = CONF_IMAGE_DIR):
"""
will attempt to find a file that matches glob in given directory with any file extension,
otherwise will try to find a `role-unknown` image
returns static file url
"""
if file := find_image_in_dir(glob, dir):
return image_static_url(file)
if glob != "role-unknown" and (file := find_image_in_dir("role-unknown", dir)):
return image_static_url(file)
if dir != IMAGE_DIR and (file := find_image_in_dir("role-unknown", IMAGE_DIR)):
return image_static_url(file)
return ""
@dataclass
class Role:
slug: str
name: str
def get_model_slug(model: Type[Model]):
return camel_case_to_spaces(model.__name__).replace(" ", "-")
def get_model_role(model: Type[Model]) -> Role:
return Role(
slug=get_model_slug(model),
name=re_camel_case.sub(r" \1", model.__name__),
)
+765 -5
View File
@@ -1,9 +1,769 @@
from django.shortcuts import get_object_or_404, render
from django.views.generic import View
import json
from functools import reduce
from typing import DefaultDict, Dict, Optional, Union
import time
from utilities.htmx import is_htmx
from circuits.models import Circuit, CircuitTermination
from dcim.models import (
Cable,
CableTermination,
Device,
device_components,
DeviceRole,
FrontPort,
Interface,
PowerFeed,
PowerPanel,
RearPort,
)
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, QuerySet
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
from django.shortcuts import render
from django.views.generic import View
from extras.models import Tag
from wireless.models import WirelessLink
from netbox_topology_views.filters import DeviceFilterSet
from netbox_topology_views.forms import DeviceFilterForm
from netbox_topology_views.models import RoleImage
from netbox_topology_views.utils import (
CONF_IMAGE_DIR,
find_image_url,
get_model_role,
get_model_slug,
image_static_url,
)
supported_termination_types = [
"interface",
"front port",
"rear port",
"power outlet",
"power port",
"console port",
"console server port",
]
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
is_device = isinstance(entity, Device)
query = (
{"object_id": entity.device_role_id}
if is_device
else {"content_type_id": ContentType.objects.get_for_model(entity).pk}
)
try:
return RoleImage.objects.get(**query).get_image_url()
except RoleImage.DoesNotExist:
return find_image_url(
entity.device_role.slug if is_device else get_model_slug(entity.__class__)
)
def create_node(
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool
):
node = {}
node_content = ""
if isinstance(device, Circuit):
dev_name = f"Circuit {device.cid}"
node["id"] = f"c{device.pk}"
if device.provider is not None:
node_content += (
f"<tr><th>Provider: </th><td>{device.provider.name}</td></tr>"
)
if device.type is not None:
node_content += f"<tr><th>Type: </th><td>{device.type.name}</td></tr>"
elif isinstance(device, PowerPanel):
dev_name = f"Power Panel {device.pk}"
node["id"] = f"p{device.pk}"
if device.site is not None:
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
if device.location is not None:
node_content += (
f"<tr><th>Location: </th><td>{device.location.name}</td></tr>"
)
elif isinstance(device, PowerFeed):
dev_name = f"Power Feed {device.pk}"
node["id"] = f"f{device.pk}"
if device.power_panel is not None:
node_content += (
f"<tr><th>Power Panel: </th><td>{device.power_panel.name}</td></tr>"
)
if device.type is not None:
node_content += f"<tr><th>Type: </th><td>{device.type}</td></tr>"
if device.supply is not None:
node_content += f"<tr><th>Supply: </th><td>{device.supply}</td></tr>"
if device.phase is not None:
node_content += f"<tr><th>Phase: </th><td>{device.phase}</td></tr>"
if device.amperage is not None:
node_content += f"<tr><th>Amperage: </th><td>{device.amperage}</td></tr>"
if device.voltage is not None:
node_content += f"<tr><th>Voltage: </th><td>{device.voltage}</td></tr>"
else:
dev_name = device.name
if dev_name is None:
dev_name = "device name unknown"
if device.device_type is not None:
node_content += (
f"<tr><th>Type: </th><td>{device.device_type.model}</td></tr>"
)
if device.device_role.name is not None:
node_content += (
f"<tr><th>Role: </th><td>{device.device_role.name}</td></tr>"
)
if device.serial != "":
node_content += f"<tr><th>Serial: </th><td>{device.serial}</td></tr>"
if device.primary_ip is not None:
node_content += (
f"<tr><th>IP Address: </th><td>{device.primary_ip.address}</td></tr>"
)
if device.site is not None:
node_content += f"<tr><th>Site: </th><td>{device.site.name}</td></tr>"
if device.location is not None:
node_content += (
f"<tr><th>Location: </th><td>{device.location.name}</td></tr>"
)
if device.rack is not None:
node_content += f"<tr><th>Rack: </th><td>{device.rack.name}</td></tr>"
if device.position is not None:
if device.face is not None:
node_content += f"<tr><th>Position: </th><td>{device.position} ({device.face})</td></tr>"
else:
node_content += (
f"<tr><th>Position: </th><td>{device.position}</td></tr>"
)
node["id"] = device.pk
if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
node["title"] = dev_title
node["name"] = dev_name
node["label"] = dev_name
node["shape"] = "image"
node["href"] = device.get_absolute_url()
node["image"] = get_image_for_entity(device)
node["physics"] = True
if "coordinates" in device.custom_field_data:
if device.custom_field_data["coordinates"] is not None:
if ";" in device.custom_field_data["coordinates"]:
cords = device.custom_field_data["coordinates"].split(";")
node["x"] = int(cords[0])
node["y"] = int(cords[1])
node["physics"] = False
elif save_coords:
node["physics"] = False
return node
def create_edge(
edge_id: int,
termination_a: Dict,
termination_b: Dict,
circuit: Optional[Dict] = None,
cable: Optional[Cable] = None,
wireless: Optional[Dict] = None,
power: Optional[bool] = None,
interface: Optional[bool] = None,
):
cable_a_name = (
"device A name unknown"
if termination_a["termination_name"] is None
else termination_a["termination_name"]
)
cable_a_dev_name = (
"device A name unknown"
if termination_a["termination_device_name"] is None
else termination_a["termination_device_name"]
)
cable_b_name = (
"device A name unknown"
if termination_b["termination_name"] is None
else termination_b["termination_name"]
)
cable_b_dev_name = (
"cable B name unknown"
if termination_b["termination_device_name"] is None
else termination_b["termination_device_name"]
)
edge = {}
edge["id"] = edge_id
edge["from"] = termination_a["device_id"]
edge["to"] = termination_b["device_id"]
title = "Cable"
if circuit is not None:
edge["dashes"] = True
title = f"Circuit provider: {circuit['provider_name']}<br>Termination"
elif wireless is not None:
edge["dashes"] = [2, 10, 2, 10]
title = "Wireless Connection"
elif power is not None:
edge["dashes"] = [5, 5, 3, 3]
title = "Power Connection"
elif interface is not None:
title = "Interface Connection"
edge["width"] = 3
edge["dashes"] = [1, 10, 1, 10]
edge["arrows"] = {"to": {"enabled": True, "scaleFactor": 0.5}, "from": {"enabled": True, "scaleFactor": 0.5}}
edge["color"] = '#f1c232'
edge[
"title"
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}]"
if cable is not None:
edge["href"] = cable.get_absolute_url()
if hasattr(cable, 'color') and cable.color != "":
edge["color"] = "#" + cable.color
return edge
def create_circuit_termination(termination):
if isinstance(termination, CircuitTermination):
return {
"termination_name": termination.circuit.provider.name,
"termination_device_name": termination.circuit.cid,
"device_id": "c{}".format(termination.circuit.pk),
}
if (
isinstance(termination, Interface)
or isinstance(termination, FrontPort)
or isinstance(termination, RearPort)
):
return {
"termination_name": termination.name,
"termination_device_name": termination.device.name,
"device_id": termination.device.pk,
}
return None
def get_topology_data(
queryset: QuerySet,
hide_unconnected: bool,
save_coords: bool,
show_cables: bool,
show_circuit: bool,
show_logical_connections: bool,
show_power: bool,
show_wireless: bool,
):
if not queryset:
return None
nodes_devices = {}
edges = []
nodes = []
edge_ids = 0
nodes_circuits: Dict[int, Circuit] = {}
nodes_powerpanel: Dict[int, PowerPanel] = {}
nodes_powerfeed: Dict[int, PowerFeed] = {}
nodes_provider_networks = {}
cable_ids = DefaultDict(dict)
interface_ids = DefaultDict(dict)
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
"ignore_cable_type"
]
hide_single_cable_logical_conns = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"][
"hide_single_cable_logical_conns"
]
)
device_ids = [d.pk for d in queryset]
site_ids = [d.site_id for d in queryset]
if show_circuit:
circuit_terminations = CircuitTermination.objects.filter(
Q(site_id__in=site_ids) | Q(provider_network__isnull=False)
).prefetch_related("provider_network", "circuit")
for circuit_termination in circuit_terminations:
circuit_termination: CircuitTermination
if (
not hide_unconnected
and circuit_termination.circuit_id not in nodes_circuits
):
nodes_circuits[
circuit_termination.circuit.pk
] = circuit_termination.circuit
termination_a = {}
termination_b = {}
circuit_model = {}
if circuit_termination.cable is not None:
termination_a = create_circuit_termination(
circuit_termination.cable.a_terminations[0]
)
termination_b = create_circuit_termination(
circuit_termination.cable.b_terminations[0]
)
elif circuit_termination.provider_network is not None:
if (
circuit_termination.provider_network_id
not in nodes_provider_networks
):
nodes_provider_networks[
circuit_termination.provider_network.pk
] = circuit_termination.provider_network
if bool(termination_a) and bool(termination_b):
circuit_model = {
"provider_name": circuit_termination.circuit.provider.name
}
edge_ids += 1
edges.append(
create_edge(
edge_id=edge_ids,
cable=circuit_termination.cable,
circuit=circuit_model,
termination_a=termination_a,
termination_b=termination_b,
)
)
circuit_has_connections = False
for termination in [
circuit_termination.cable.a_terminations[0],
circuit_termination.cable.b_terminations[0],
]:
if not isinstance(termination, CircuitTermination):
if (
termination.device_id not in nodes_devices
and termination.device_id in device_ids
):
nodes_devices[termination.device_id] = termination.device
circuit_has_connections = True
else:
if termination.device_id in device_ids:
circuit_has_connections = True
if circuit_has_connections and hide_unconnected:
if circuit_termination.circuit_id not in nodes_circuits:
nodes_circuits[
circuit_termination.circuit.pk
] = circuit_termination.circuit
for d in nodes_circuits.values():
nodes.append(create_node(d, save_coords))
if show_power:
power_panels_ids = PowerPanel.objects.filter(
Q(site_id__in=site_ids)
).values_list("pk", flat=True)
power_feeds: QuerySet[PowerFeed] = PowerFeed.objects.filter(
Q(power_panel_id__in=power_panels_ids)
)
for power_feed in power_feeds:
if not hide_unconnected or (
hide_unconnected and power_feed.cable_id is not None
):
if power_feed.power_panel_id not in nodes_powerpanel:
nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel
power_link_name = ""
if power_feed.pk not in nodes_powerfeed:
if hide_unconnected:
if power_feed.link_peers[0].device_id in device_ids:
nodes_powerfeed[power_feed.pk] = power_feed
power_link_name = power_feed.link_peers[0].name
else:
nodes_powerfeed[power_feed.pk] = power_feed
edge_ids += 1
termination_a = {
"termination_name": power_feed.power_panel.name,
"termination_device_name": "",
"device_id": f"p{power_feed.power_panel_id}",
}
termination_b = {
"termination_name": power_feed.name,
"termination_device_name": power_link_name,
"device_id": f"f{power_feed.pk}",
}
edges.append(
create_edge(
edge_id=edge_ids,
termination_a=termination_a,
termination_b=termination_b,
power=True,
)
)
if power_feed.cable_id is not None:
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
for d in nodes_powerfeed.values():
nodes.append(create_node(d, save_coords))
for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords))
if show_logical_connections:
interfaces = Interface.objects.filter(
Q(_path__is_complete=True) & Q(device_id__in=device_ids)
)
for interface in interfaces:
# print('{} {} {} {}'.format(interface.device.name, interface.name, interface._path.destinations[0].device.name, interface._path.destinations[0].name))
for destination in interface._path.destinations:
if isinstance(destination, device_components.Interface):
if destination.device.id not in device_ids:
# print('Destination interface not in device queryset, ignoring')
continue
if destination.id in interface_ids:
# we've already captured the destination interface, ignore this connection
# print('Destination interface already exists, ignoring')
continue
if hide_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables:
# interface connection is the same as the cable connection, ignore this connection
continue
interface_ids[interface.id]=interface
edge_ids += 1
termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id }
termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id }
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=True))
nodes_devices[interface.device.id] = interface.device
nodes_devices[destination.device.id] = destination.device
if show_cables:
links: QuerySet[CableTermination] = CableTermination.objects.filter(
Q(_device_id__in=device_ids)
).select_related("termination_type")
for link in links:
if link.termination_type.name in ignore_cable_type:
continue
# Normal device cables
if link.termination_type.name in supported_termination_types:
complete_link = False
if link.cable_end == "A":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "B" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["B"] is not None:
complete_link = True
elif link.cable_end == "B":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "A" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["A"] is not None:
complete_link = True
else:
print("Unkown cable end")
cable_ids[link.cable_id][link.cable_end] = link
if complete_link:
edge_ids += 1
if isinstance(cable_ids[link.cable_id]["B"], CableTermination):
if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices:
nodes_devices[
cable_ids[link.cable_id]["B"]._device_id
] = cable_ids[link.cable_id]["B"].termination.device
termination_b = {
"termination_name": cable_ids[link.cable_id][
"B"
].termination.name,
"termination_device_name": cable_ids[link.cable_id][
"B"
].termination.device.name,
"device_id": cable_ids[link.cable_id][
"B"
].termination.device_id,
}
else:
termination_b = cable_ids[link.cable_id]["B"]
if isinstance(cable_ids[link.cable_id]["A"], CableTermination):
if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices:
nodes_devices[
cable_ids[link.cable_id]["A"]._device_id
] = cable_ids[link.cable_id]["A"].termination.device
termination_a = {
"termination_name": cable_ids[link.cable_id][
"A"
].termination.name,
"termination_device_name": cable_ids[link.cable_id][
"A"
].termination.device.name,
"device_id": cable_ids[link.cable_id][
"A"
].termination.device_id,
}
else:
termination_a = cable_ids[link.cable_id]["A"]
edges.append(
create_edge(
edge_id=edge_ids,
cable=link.cable,
termination_a=termination_a,
termination_b=termination_b,
)
)
if show_wireless:
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
Q(_interface_a_device_id__in=device_ids)
& Q(_interface_b_device_id__in=device_ids)
)
for wlan_link in wlan_links:
if wlan_link.interface_a.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_a.device.pk
] = wlan_link.interface_a.device
if wlan_link.interface_b.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_b.device.pk
] = wlan_link.interface_b.device
termination_a = {
"termination_name": wlan_link.interface_a.name,
"termination_device_name": wlan_link.interface_a.device.name,
"device_id": wlan_link.interface_a.device_id,
}
termination_b = {
"termination_name": wlan_link.interface_b.name,
"termination_device_name": wlan_link.interface_b.device.name,
"device_id": wlan_link.interface_b.device_id,
}
wireless = {"ssid": wlan_link.ssid}
edge_ids += 1
edges.append(
create_edge(
edge_id=edge_ids,
cable=wlan_link,
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
)
)
for qs_device in queryset:
if qs_device.pk not in nodes_devices and not hide_unconnected:
nodes_devices[qs_device.pk] = qs_device
results = {}
for d in nodes_devices.values():
nodes.append(create_node(d, save_coords))
results["nodes"] = nodes
results["edges"] = edges
return results
class TopologyHomeView(PermissionRequiredMixin, View):
permission_required = ("dcim.view_site", "dcim.view_device")
class TopologyHomeView(View):
"""
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,
},
)
-4118
View File
File diff suppressed because it is too large Load Diff
-20
View File
@@ -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"
}
}
+3
View File
@@ -0,0 +1,3 @@
djangorestframework
django-filter
black
+18 -11
View File
@@ -1,22 +1,29 @@
from setuptools import setup, find_packages
from pathlib import Path
from setuptools import find_packages, setup
readme = Path(__file__).parent / "README.md"
long_description = readme.read_text()
setup(
name='netbox-topology-views',
version='0.2',
description='An NetBox plugin to create Topology maps',
url='https://github.com/mattieserver/netbox-topology-views',
author='Mattijs Vanhaverbeke',
license='Apache 2.0',
name="netbox-topology-views",
version="3.2.0",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/mattieserver/netbox-topology-views",
author="Mattijs Vanhaverbeke",
license="Apache 2.0",
install_requires=[],
packages=find_packages(),
include_package_data=True,
keywords=["netbox-plugin"],
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
],
project_urls={
'Source': 'https://github.com/mattieserver/netbox-topology-views',
"Source": "https://github.com/mattieserver/netbox-topology-views",
},
)