Compare commits

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

* Added general options

* Added show_single_cable_logical_conns and draw_default_layout

* Added ignore_cable_type to IndividualOptions

* Corrected permission, tiny improvements

* Add preselected_device_roles to IndividualOptions

* Add preselected_tags

* Updated documentation

* Update README.md

* Corrected queryset for preselected_tags

* Update README.md

* move supported_termination_types to inside the function to prevent migration issues

* removed general options

* added new migration script

---------

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

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

* build js

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

* Fixed 'Hide Unconnected' and other bugs

* rename var + extra check

* Add filter selection image for readme

* Added guidance on using the selection options

* Renaming to selection_options

* Renamed filter_selection to selection_options

* Update README.md

* Bug fix

* fix parameter order

* differentiate line type for interface connections

* allow both cables and logical connections

* Change logical connection line type

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

* Hide redundant connections, configurable options

* updated readme + removed visjs setting from settings

* Update README.md

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

* updated readme for 'Show Logical Connections'

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

* add filter field

* fixed model

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

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

* used new PluginMenu class
2022-12-21 17:03:47 +01:00
Mario 2f65614fcb Merge pull request #226 from dreng/pull_request_template
Create pull_request_template.md
2022-12-20 19:54:44 +01:00
Mario 2722dfa793 Create pull_request_template.md 2022-12-20 19:38:36 +01:00
26 changed files with 741 additions and 507 deletions
+24
View File
@@ -0,0 +1,24 @@
<!--
IMPORTANT:
==========
Thank you for contributing to NetBox Topology Views! Please
note that our contribution guidelines require a feature
request or bug report to be labeled "status: accepted". This
helps avoid waste time and effort on a proposed change that
we might not be able to accept. If a feature request or bug
report is labeled "status: needs owner", feel free to raise
your hand if you'd like to take work on it and we will
happily assign you to the issue.
Please prefix the title of your pull request according to
it's type. You should use "Closes #FEATURE_ID: ..." for feature
requests and "Fixes #ISSUE_ID: ..." for issues. This helps
us closing issues automatically after merging.
To sum it up:
1. Create Bug Report or Feature Request
2. Wait until the issue has been labeled "status: accepted"
3. Start working on the issue in a seperate branch
3. Create a pull request and name the title with a prefix
according to the issue ID.
-->
-93
View File
@@ -1,93 +0,0 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '35 10 * * *'
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25
with:
cosign-release: 'v1.9.0'
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
COSIGN_EXPERIMENTAL: "true"
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: cosign sign ${{ steps.meta.outputs.tags }}@${{ steps.build-and-push.outputs.digest }}
-4
View File
@@ -1,4 +0,0 @@
FROM netboxcommunity/netbox:latest
COPY ./docker/plugin_requirements.txt /
RUN /opt/netbox/venv/bin/pip install --no-warn-script-location -r /plugin_requirements.txt
+44 -8
View File
@@ -12,6 +12,8 @@ Support to filter on name, site, tag and device role.
## Install
**_NOTE:_** For docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
The plugin is available as a Python package and can be installed with pip.
Run `pip install netbox-topology-views` in your virtual env.
@@ -44,6 +46,7 @@ python3 manage.py collectstatic --no-input
| netbox version | netbox-topology-views version |
| -------------- | ----------------------------- |
| >= 3.4.0 | >= v3.X.X |
| >= 3.3.0 | >= v3.0.0 |
| >= 3.2.0 | >= v1.1.0 |
| >= 3.1.8 | >= v1.0.0 |
@@ -64,14 +67,17 @@ You might also set the `always_save_coordinates` option to True.
## Configure
If you want to override the default values configure the `PLUGINS_CONFIG` in your `netbox configuration.py`.
All individual options can be assigned a default value per user directly in the plugin. The default value can be overridden on the filter page.
The remaining options must be configured in the `PLUGINS_CONFIG` section of your `netbox/configuration.py`.
Example:
```
PLUGINS_CONFIG = {
'netbox_topology_views': {
'preselected_device_roles': ['Router', 'Firewall'],
'enable_circuit_terminations': True
'static_image_directory': 'netbox_topology_views/img',
'allow_coordinates_saving': True,
'always_save_coordinates': True
}
}
```
@@ -79,12 +85,8 @@ PLUGINS_CONFIG = {
| Setting | Default value | Description |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| static_image_directory | netbox_topology_views/img | (str or pathlib.Path) Specifies the location that images will be loaded from by default. Must be within `STATIC_ROOT` |
| preselected_device_roles | ['Firewall', 'Router', 'Distribution Switch', 'Core Switch', 'Internal Switch', 'Access Switch', 'Server', 'Storage', 'Backup', 'Wireless AP'] | The full name of the device roles you want to pre select in the global view. Note that this is case sensitive |
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates |
| always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default |
| ignore_cable_type | [] | The cable types that you want to ignore in the views |
| preselected_tags | [] | The name of tags you want to preload |
| draw_default_layout | False | (bool) Set to True if you want to load draw the topology on the initial load (when you go to the topology plugin page) |
### Custom Images
@@ -94,6 +96,31 @@ To change image with associated device use the `Images` page - it allows to map
Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plugins/netbox_topology_views/` to view your topologies
Select your options for the topology view:
![selection_options](https://user-images.githubusercontent.com/20901110/220410144-5633d24c-6383-4cba-8d6d-ba77ef3d3cdd.png)
<dl>
<dt>Save Coordinates</dt>
<dd>Save the coordinates of devices in the topology view.</dd>
<dt>Show Unconnected</dt>
<dd>Show devices that have no connections or for which no connection is displayed. This option depends on other parameters like 'Show Cables' and 'Show Logical Connections'.</dd>
<dt>Show Circuit Terminations</dt>
<dd>Show connections which end at a circuit termination in the topology view. These connections are displayed as blue dashed lines.</dd>
<dt>Show Cables</dt>
<dd>Show connections between interfaces, front / rear ports, etc., that are connected with one or more cables. These connections are displayed as solid lines in the color of the cable.</dd>
<dt>Show Logical Connections</dt>
<dd>Show logical connections between interfaces (referred to as Interface Connections in NetBox) in the topology view. Where the path between
interfaces includes multiple cables (e.g., via patch panels), only the end interface connections are shown, not the
intermediate front / rear port connections, etc. This is similar to what was referred to as 'end-to-end' connections in previous versions. These connections are displayed as yellow dotted lines.</dd>
<dt>Show redundant Cable and Locigal Connection</dt>
<dd>Shows a logical connection (in addition to a cable), even if a cable is directly connected. Leaving this option disabled prevents that redundant display. This option only has an effect if 'Show Logical Connections' is activated.</dd>
<dt>Show Power Feeds</dt>
<dd>Displays connections between power outlets and power ports. These connections are displayed as solid lines in the color of the cable. This option depends on 'Show Cables'.</dd>
<dt>Show Wireless Links</dt>
<dd>Displays wireless connections. These connections are displayed as blue dotted lines.</dd>
</dl>
### Update
Run `pip install netbox-topology-views --upgrade` in your venv.
@@ -108,12 +135,21 @@ Clear you browser cache.
### Permissions
To view `/plugins/topology-views/` you need the following permissions:
To view `/plugins/netbox_topology-views/topology` you need the following permissions:
+ dcim | device | can view device
+ dcim | site | can view site
+ extras | tag | can view tag
+ dcim | device role | can view device role
To view `/plugins/netbox_topology-views/image`:
+ dcim | site | view
+ dcim | device role | view
+ dcim | device role | add
+ dcim | device role | change
To view `/plugins/netbox_topology-views/individualoptions`:
+ netbox topology views | individualoptions | change
## Icons info
Power icons created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/power).
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

-1
View File
@@ -1 +0,0 @@
netbox-topology-views
+2 -16
View File
@@ -5,30 +5,16 @@ class TopologyViewsConfig(PluginConfig):
name = "netbox_topology_views"
verbose_name = "Topology views"
description = "An plugin to render topology maps"
version = "3.1.1"
version = "3.3.0"
author = "Mattijs Vanhaverbeke"
author_email = "author@example.com"
base_url = "netbox_topology_views"
required_settings = []
default_settings = {
"preselected_device_roles": [
"Firewall",
"Router",
"Distribution Switch",
"Core Switch",
"Internal Switch",
"Access Switch",
"Server",
"Storage",
"Backup",
"Wireless AP",
],
"ignore_cable_type": [],
"static_image_directory": "netbox_topology_views/img",
"allow_coordinates_saving": False,
"always_save_coordinates": False,
"preselected_tags": [],
"draw_default_layout": False,
}
+7 -1
View File
@@ -1,7 +1,8 @@
from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from netbox_topology_views.models import RoleImage
from netbox_topology_views.models import RoleImage, IndividualOptions
class TopologyDummySerializer(ModelSerializer):
@@ -20,3 +21,8 @@ class DeviceRoleSerializer(ModelSerializer):
class Meta:
model = DeviceRole
fields = ("name", "slug", "color", "vm_role", "description")
class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta:
model = IndividualOptions
fields = ("ignore_cable_type", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_circuit", "show_power", "show_wireless", "draw_default_layout")
+147 -15
View File
@@ -12,17 +12,14 @@ from dcim.choices import DeviceStatusChoices
from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm
from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm
from utilities.forms import (
TagFilterField,
DynamicModelMultipleChoiceField,
MultipleChoiceField,
widgets,
)
allow_coordinates_saving = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"]
)
from .models import IndividualOptions
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device
@@ -31,10 +28,15 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
None,
(
"q",
"hide_unconnected",
"filter_id",
"save_coords",
"show_unconnected",
"show_cables",
"show_circuit",
"show_logical_connections",
"show_single_cable_logical_conns",
"show_power",
"show_wireless",
),
),
(
@@ -100,8 +102,32 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
},
label=_("Rack"),
)
hide_unconnected = forms.BooleanField(
label=_("Hide Unconnected"), required=False, initial=False
status = MultipleChoiceField(
choices=DeviceStatusChoices, required=False, label=_("Device Status")
)
tag = TagFilterField(model)
# options
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
required=False,
disabled=(not settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] or settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"]),
initial=(settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"])
)
show_unconnected = forms.BooleanField(
label=_("Show Unconnected"), required=False, initial=False
)
show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"), required=False, initial=False
)
show_single_cable_logical_conns = forms.BooleanField(
label =_("Show redundant Cable and Locigal Connection"), required=False, initial=False
)
show_cables = forms.BooleanField(
label =_("Show Cables"), required=False, initial=False
)
show_wireless = forms.BooleanField(
label =_("Show Wireless Links"), required=False, initial=False
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"), required=False, initial=False
@@ -109,12 +135,118 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
show_power = forms.BooleanField(
label=_("Show Power Feeds"), required=False, initial=False
)
save_coords = forms.BooleanField(
label=_("Save Coordinates"),
class IndividualOptionsForm(NetBoxModelForm):
fieldsets = (
(
None,
(
"user_id",
"ignore_cable_type",
"preselected_device_roles",
"preselected_tags",
"show_unconnected",
"show_cables",
"show_circuit",
"show_logical_connections",
"show_single_cable_logical_conns",
"show_power",
"show_wireless",
"draw_default_layout",
),
),
)
user_id = forms.CharField(widget=forms.HiddenInput())
ignore_cable_type = MultipleChoiceField(
label=_("Ignore Termination Types"),
required=False,
choices=IndividualOptions.CHOICES,
help_text=_("Choose Termination Types that you want to be ignored. "
"If any ignored Termination Type is part of a connection, the "
"cable is not displayed.")
)
preselected_device_roles = DynamicModelMultipleChoiceField(
label=_("Preselected Device Role"),
queryset=DeviceRole.objects.all(),
required=False,
disabled=(not allow_coordinates_saving),
help_text=_("Select Device Roles that you want to have "
"preselected in the filter tab.")
)
status = MultipleChoiceField(
choices=DeviceStatusChoices, required=False, label=_("Device Status")
preselected_tags = forms.ModelMultipleChoiceField(
label=_("Preselected Tags"),
queryset=Device.tags.all(),
required=False,
widget=widgets.StaticSelectMultiple,
help_text=_("Select Tags that you want to have "
"preselected in the filter tab.")
)
tag = TagFilterField(model)
show_unconnected = forms.BooleanField(
label=_("Show Unconnected"),
required=False,
initial=False,
help_text=_("Draws devices that have no connections or for which no "
"connection is displayed. This option depends on other parameters "
"like 'Show Cables' and 'Show Logical Connections'.")
)
show_cables = forms.BooleanField(
label =_("Show Cables"),
required=False,
initial=False,
help_text=_("Displays connections between interfaces that are connected "
"with one or more cables. These connections are displayed as solid "
"lines in the color of the cable.")
)
show_logical_connections = forms.BooleanField(
label =_("Show Logical Connections"),
required=False,
initial=False,
help_text=_("Displays connections between devices that are not "
"directly connected (e.g. via patch panels). These connections "
"are displayed as yellow dotted lines.")
)
show_single_cable_logical_conns = forms.BooleanField(
label = ("Show redundant Cable and Locigal Connection"),
required = False,
initial=False,
help_text=_("Shows a logical connection (in addition to a cable), "
"even if a cable is directly connected. Leaving this option "
"disabled prevents that redundant display. This option only "
"has an effect if 'Show Logical Connections' is activated.")
)
show_circuit = forms.BooleanField(
label=_("Show Circuit Terminations"),
required=False,
initial=False,
help_text=_("Displays connections between circuit terminations. "
"These connections are displayed as blue dashed lines.")
)
show_power = forms.BooleanField(
label=_("Show Power Feeds"),
required=False,
initial=False,
help_text=_("Displays connections between power outlets and power "
"ports. These connections are displayed as solid lines in the "
"color of the cable. This option depends on 'Show Cables'.")
)
show_wireless = forms.BooleanField(
label =_("Show Wireless Links"),
required=False,
initial=False,
help_text=_("Displays wireless connections. These connections are "
"displayed as blue dotted lines.")
)
draw_default_layout = forms.BooleanField(
label = ("Draw Default Layout"),
required=False,
initial=False,
help_text=_("Enable this option if you want to draw the topology on "
"the initial load (when you go to the topology plugin page).")
)
class Meta:
model = IndividualOptions
fields = [
'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_circuit', 'show_power', 'show_wireless', 'draw_default_layout'
]
@@ -0,0 +1,42 @@
# Generated by Django 4.1.4 on 2023-03-19 14:13
from django.db import migrations, models
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('dcim', '0167_module_status'),
('extras', '0084_staging'),
('netbox_topology_views', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='IndividualOptions',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('user_id', models.IntegerField(null=True, unique=True)),
('ignore_cable_type', models.CharField(blank=True, max_length=255)),
('show_unconnected', models.BooleanField(default=False)),
('show_cables', models.BooleanField(default=False)),
('show_logical_connections', models.BooleanField(default=False)),
('show_single_cable_logical_conns', models.BooleanField(default=False)),
('show_circuit', models.BooleanField(default=False)),
('show_power', models.BooleanField(default=False)),
('show_wireless', models.BooleanField(default=False)),
('draw_default_layout', models.BooleanField(default=False)),
('preselected_device_roles', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_device', related_name='+', to='dcim.devicerole')),
('preselected_tags', models.ManyToManyField(blank=True, db_table='netbox_topology_views_individualoptions_preselected_tag', related_name='+', to='extras.tag')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'abstract': False,
},
),
]
+61
View File
@@ -2,10 +2,12 @@ from pathlib import Path
from typing import Optional
from dcim.models import DeviceRole
from extras.models import Tag
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.templatetags.static import static
from netbox.models import NetBoxModel
from netbox.models.features import (
ChangeLoggingMixin,
ExportTemplatesMixin,
@@ -91,3 +93,62 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
except ValueError:
return self.get_default_image(dir)
return static(f"/{self.image}")
class IndividualOptions(NetBoxModel):
CHOICES = (
('interface', 'interface'),
('front port', 'front port'),
('rear port', 'rear port'),
('power outlet', 'power outlet'),
('power port', 'power port'),
('console port', 'console port'),
('console server port', 'console server port'),
)
user_id = models.IntegerField(
null=True,
unique=True
)
ignore_cable_type = models.CharField(
max_length = 255,
blank = True,
)
preselected_device_roles = models.ManyToManyField(
to='dcim.DeviceRole',
related_name='+',
blank=True,
db_table='netbox_topology_views_individualoptions_preselected_device',
)
preselected_tags = models.ManyToManyField(
to='extras.Tag',
related_name='+',
blank=True,
db_table='netbox_topology_views_individualoptions_preselected_tag',
)
show_unconnected = models.BooleanField(
default=False
)
show_cables = models.BooleanField(
default=False
)
show_logical_connections = models.BooleanField(
default=False
)
show_single_cable_logical_conns = models.BooleanField(
default=False
)
show_circuit = models.BooleanField(
default=False
)
show_power = models.BooleanField(
default=False
)
show_wireless = models.BooleanField(
default=False
)
draw_default_layout = models.BooleanField(
default=False
)
def __str___(self):
return f"{self.user_id}"
+12 -5
View File
@@ -1,8 +1,15 @@
from extras.plugins import PluginMenuItem
from extras.plugins import PluginMenu, PluginMenuItem
menu_items = (
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
PluginMenuItem(
link="plugins:netbox_topology_views:images", link_text="Topology Images"
menu = PluginMenu(
label='Topology Views',
icon_class="mdi mdi-sitemap",
groups=(
('TOPOLOGY', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),),
('PREFERENCES',
(
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"),
PluginMenuItem(link="plugins:netbox_topology_views:individualoptions", link_text="Individual Options"),
),
),
),
)
@@ -1 +1 @@
#visgraph{height:70vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
#visgraph{height:64vh}html[data-netbox-color-mode=dark] #visgraph{background-color:#212529}.image-dropdown img{width:64px;height:64px}.image-dropdown-content{display:flex;flex-wrap:wrap;gap:.5rem;padding-inline:.5rem;width:50vw;max-width:32rem}.image-dropdown-content>img{cursor:pointer}
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
#visgraph {
height: 70vh;
height: 64vh;
}
html[data-netbox-color-mode=dark] #visgraph {
+10 -3
View File
@@ -100,9 +100,16 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
})
graph.on('doubleClick', (params) => {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
if (params.nodes.length > 0) {
params.nodes.forEach((node) => {
window.open(nodes.get(node).href, '_blank')
})
}
else {
params.edges.forEach((edge) => {
window.open(edges.get(edge).href, '_blank')
})
}
})
})()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "netbox_topology_views",
"version": "3.1.1",
"version": "3.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1,7 +1,7 @@
{
"private": true,
"name": "netbox_topology_views",
"version": "3.1.1",
"version": "3.3.0",
"scripts": {
"bundle": "node bundle.js",
"bundle:styles": "node bundle.js --styles",
+72 -177
View File
@@ -4,44 +4,39 @@
"@egjs/hammerjs@^2.0.0":
version "2.0.17"
resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"
resolved "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz"
integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==
dependencies:
"@types/hammerjs" "^2.0.36"
"@esbuild/linux-loong64@0.14.53":
version "0.14.53"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6"
integrity sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==
"@types/hammerjs@^2.0.36":
version "2.0.41"
resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa"
resolved "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz"
integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
version "3.1.3"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
"chokidar@>=3.0.0 <4.0.0":
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
@@ -56,92 +51,17 @@ braces@~3.0.2:
component-emitter@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
resolved "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
esbuild-android-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz#259bc3ef1399a3cad8f4f67c40ee20779c4de675"
integrity sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==
esbuild-android-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e"
integrity sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==
esbuild-darwin-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a"
integrity sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==
esbuild-darwin-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9"
integrity sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==
esbuild-freebsd-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0"
integrity sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==
esbuild-freebsd-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9"
integrity sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==
esbuild-linux-32@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5"
integrity sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==
esbuild-linux-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4"
integrity sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==
esbuild-linux-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe"
integrity sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==
esbuild-linux-arm@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf"
integrity sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==
esbuild-linux-mips64le@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f"
integrity sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==
esbuild-linux-ppc64le@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139"
integrity sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==
esbuild-linux-riscv64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00"
integrity sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==
esbuild-linux-s390x@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129"
integrity sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==
esbuild-netbsd-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz#e86d0efd0116658be335492ed12e66b26b4baf52"
integrity sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==
esbuild-openbsd-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b"
integrity sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==
esbuild-linux-64@0.14.54:
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz"
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
esbuild-sass-plugin@^1.5.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz#76babbd6d4c87ffd14259fd364d446f857b3f513"
resolved "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-1.8.2.tgz"
integrity sha512-ZBjONsRSpmzMKvxSNohnNXCNaBBVlYLqYhyZtN8leGGkJktCvVMKC6g9V5TWIemk1SEaW2XK+YFxz3Whw3+YYw==
dependencies:
esbuild "^0.14.5"
@@ -149,155 +69,130 @@ esbuild-sass-plugin@^1.5.2:
resolve "^1.20.0"
sass "^1.45.0"
esbuild-sunos-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8"
integrity sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==
esbuild-windows-32@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709"
integrity sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==
esbuild-windows-64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af"
integrity sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==
esbuild-windows-arm64@0.14.53:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8"
integrity sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==
esbuild@^0.12.24:
version "0.12.29"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.29.tgz#be602db7c4dc78944a9dbde0d1ea19d36c1f882d"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.12.29.tgz"
integrity sha512-w/XuoBCSwepyiZtIRsKsetiLDUVGPVw1E/R3VTFSecIy8UR7Cq3SOtwKHJMFoVqqVG36aGkzh4e8BvpO1Fdc7g==
esbuild@^0.14.5:
version "0.14.53"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.53.tgz#20b1007f686e8584f2a01a1bec5a37aac9498ce4"
integrity sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==
version "0.14.54"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz"
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
optionalDependencies:
"@esbuild/linux-loong64" "0.14.53"
esbuild-android-64 "0.14.53"
esbuild-android-arm64 "0.14.53"
esbuild-darwin-64 "0.14.53"
esbuild-darwin-arm64 "0.14.53"
esbuild-freebsd-64 "0.14.53"
esbuild-freebsd-arm64 "0.14.53"
esbuild-linux-32 "0.14.53"
esbuild-linux-64 "0.14.53"
esbuild-linux-arm "0.14.53"
esbuild-linux-arm64 "0.14.53"
esbuild-linux-mips64le "0.14.53"
esbuild-linux-ppc64le "0.14.53"
esbuild-linux-riscv64 "0.14.53"
esbuild-linux-s390x "0.14.53"
esbuild-netbsd-64 "0.14.53"
esbuild-openbsd-64 "0.14.53"
esbuild-sunos-64 "0.14.53"
esbuild-windows-32 "0.14.53"
esbuild-windows-64 "0.14.53"
esbuild-windows-arm64 "0.14.53"
"@esbuild/linux-loong64" "0.14.54"
esbuild-android-64 "0.14.54"
esbuild-android-arm64 "0.14.54"
esbuild-darwin-64 "0.14.54"
esbuild-darwin-arm64 "0.14.54"
esbuild-freebsd-64 "0.14.54"
esbuild-freebsd-arm64 "0.14.54"
esbuild-linux-32 "0.14.54"
esbuild-linux-64 "0.14.54"
esbuild-linux-arm "0.14.54"
esbuild-linux-arm64 "0.14.54"
esbuild-linux-mips64le "0.14.54"
esbuild-linux-ppc64le "0.14.54"
esbuild-linux-riscv64 "0.14.54"
esbuild-linux-s390x "0.14.54"
esbuild-netbsd-64 "0.14.54"
esbuild-openbsd-64 "0.14.54"
esbuild-sunos-64 "0.14.54"
esbuild-windows-32 "0.14.54"
esbuild-windows-64 "0.14.54"
esbuild-windows-arm64 "0.14.54"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
immutable@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef"
resolved "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz"
integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
version "2.11.0"
resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz"
integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
dependencies:
has "^1.0.3"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.1, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
keycharm@^0.4.0:
"keycharm@^0.2.0 || ^0.3.0 || ^0.4.0", keycharm@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/keycharm/-/keycharm-0.4.0.tgz#8d684ea9cc01379a07fbddee33ff32d97f5ae2a7"
resolved "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz"
integrity sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
resolve@^1.20.0:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
dependencies:
is-core-module "^2.9.0"
@@ -305,9 +200,9 @@ resolve@^1.20.0:
supports-preserve-symlinks-flag "^1.0.0"
sass@^1.45.0:
version "1.54.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.1.tgz#4f72ef57ce2a6c3251f4e2c75eee9a0c19e09eb5"
integrity sha512-GHJJr31Me32RjjUBagyzx8tzjKBUcDwo5239XANIRBq0adDu5iIG0aFO0i/TBb/4I9oyxkEv44nq/kL1DxdDhA==
version "1.56.2"
resolved "https://registry.npmjs.org/sass/-/sass-1.56.2.tgz"
integrity sha512-ciEJhnyCRwzlBCB+h5cCPM6ie/6f8HrhZMQOf5vlU60Y1bI1rx5Zb0vlDZvaycHsg/MqFfF1Eq2eokAa32iw8w==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@@ -315,42 +210,42 @@ sass@^1.45.0:
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz"
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
uuid@^8.0.0:
"uuid@^3.4.0 || ^7.0.0 || ^8.0.0", "uuid@^7.0.0 || ^8.0.0", uuid@^8.0.0:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
vis-data@^7.1.4:
vis-data@^7.0.0, vis-data@^7.1.4:
version "7.1.4"
resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.4.tgz#90e5e796a79e1901de14c0808fb32a1a0735c1dc"
resolved "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz"
integrity sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==
vis-network@^9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-9.1.2.tgz#ddaca300a9764bbf1646a0aa03eb28fe55823ca8"
resolved "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz"
integrity sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==
vis-util@^5.0.3:
"vis-util@^4.0.0 || ^5.0.0", vis-util@^5.0.1, vis-util@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/vis-util/-/vis-util-5.0.3.tgz#6a9d67c6ed92f2bfa67e50c20166bb5f59ac07bf"
resolved "https://registry.npmjs.org/vis-util/-/vis-util-5.0.3.tgz"
integrity sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==
@@ -20,12 +20,18 @@
</div>
</div>
<div class="panel-body" >
<div style="width: 500px; height: 500px;">
<div style="max-width: 900px; height: 500px;">
<div id="visgraph" style="width: 100%; height: 100%;">
</div>
</div>
</div>
<style type="text/css">
.modal-dialog {
max-width: 902px !important;
}
</style>
<script type="text/javascript">
const brokenImage = '{{ broken_image }}';
const topologyData = {{ topology_data | safe }};
@@ -48,6 +48,12 @@
{% with config=settings.PLUGINS_CONFIG.netbox_topology_views %}
<div class="tab-content">
{# Applied filters #}
{% if filter_form %}
{% applied_filters model filter_form request.GET %}
<hr style="margin-top: 0px;">
{% endif %}
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
<div class="panel-body">
<div id="visgraph"></div>
@@ -0,0 +1,5 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Topology Views Individual Options{% endblock %}
{% block tabs %} {% endblock tabs %}
@@ -1,4 +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">
<a role="button" class="btn btn-sm btn-primary" title="Topology" href="#" hx-get="/plugins/netbox_topology_views/topology/?site_id={{ object.id }}&show_cables=on" hx-target="#htmx-modal-content" data-bs-toggle="modal" data-bs-target="#htmx-modal">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
Topology
</a>
+1
View File
@@ -9,4 +9,5 @@ urlpatterns = (
path("", RedirectView.as_view(url="topology/", permanent=True)),
path("topology/", views.TopologyHomeView.as_view(), name="home"),
path("images/", views.TopologyImagesView.as_view(), name="images"),
path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"),
)
+294 -176
View File
@@ -9,6 +9,7 @@ from dcim.models import (
Cable,
CableTermination,
Device,
device_components,
DeviceRole,
FrontPort,
Interface,
@@ -17,9 +18,11 @@ from dcim.models import (
RearPort,
)
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, QuerySet
from django.db.models.functions import Lower
from django.http import HttpRequest, HttpResponseRedirect, QueryDict
from django.shortcuts import render
from django.views.generic import View
@@ -27,8 +30,8 @@ from extras.models import Tag
from wireless.models import WirelessLink
from netbox_topology_views.filters import DeviceFilterSet
from netbox_topology_views.forms import DeviceFilterForm
from netbox_topology_views.models import RoleImage
from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm
from netbox_topology_views.models import RoleImage, IndividualOptions
from netbox_topology_views.utils import (
CONF_IMAGE_DIR,
find_image_url,
@@ -37,15 +40,6 @@ from netbox_topology_views.utils import (
image_static_url,
)
supported_termination_types = [
"interface",
"front port",
"rear port",
"power outlet",
"power port",
"console port",
"console server port",
]
def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
@@ -177,6 +171,7 @@ def create_edge(
cable: Optional[Cable] = None,
wireless: Optional[Dict] = None,
power: Optional[bool] = None,
interface: Optional[Interface] = None,
):
cable_a_name = (
"device A name unknown"
@@ -217,12 +212,22 @@ def create_edge(
edge["dashes"] = [5, 5, 3, 3]
title = "Power Connection"
elif interface is not None:
title = "Interface Connection"
edge["width"] = 3
edge["dashes"] = [1, 10, 1, 10]
edge["arrows"] = {"to": {"enabled": True, "scaleFactor": 0.5}, "from": {"enabled": True, "scaleFactor": 0.5}}
edge["color"] = '#f1c232'
edge["href"] = interface.get_absolute_url() + "trace"
edge[
"title"
] = f"{title} between<br>{cable_a_dev_name} [{cable_a_name}]<br>{cable_b_dev_name} [{cable_b_name}]"
if cable is not None and cable.color != "":
edge["color"] = "#" + cable.color
if cable is not None:
edge["href"] = cable.get_absolute_url()
if hasattr(cable, 'color') and cable.color != "":
edge["color"] = "#" + cable.color
return edge
@@ -249,11 +254,21 @@ def create_circuit_termination(termination):
def get_topology_data(
queryset: QuerySet,
hide_unconnected: bool,
individualOptions: IndividualOptions,
show_unconnected: bool,
save_coords: bool,
show_cables: bool,
show_circuit: bool,
show_logical_connections: bool,
show_single_cable_logical_conns: bool,
show_power: bool,
show_wireless: bool,
):
supported_termination_types = []
for t in IndividualOptions.CHOICES:
supported_termination_types.append(t[1])
if not queryset:
return None
@@ -266,9 +281,9 @@ def get_topology_data(
nodes_powerfeed: Dict[int, PowerFeed] = {}
nodes_provider_networks = {}
cable_ids = DefaultDict(dict)
ignore_cable_type = settings.PLUGINS_CONFIG["netbox_topology_views"][
"ignore_cable_type"
]
interface_ids = DefaultDict(dict)
ignore_cable_type = individualOptions.ignore_cable_type
device_ids = [d.pk for d in queryset]
site_ids = [d.site_id for d in queryset]
@@ -280,7 +295,7 @@ def get_topology_data(
for circuit_termination in circuit_terminations:
circuit_termination: CircuitTermination
if (
not hide_unconnected
show_unconnected
and circuit_termination.circuit_id not in nodes_circuits
):
nodes_circuits[
@@ -314,6 +329,7 @@ def get_topology_data(
edges.append(
create_edge(
edge_id=edge_ids,
cable=circuit_termination.cable,
circuit=circuit_model,
termination_a=termination_a,
termination_b=termination_b,
@@ -336,7 +352,7 @@ def get_topology_data(
if termination.device_id in device_ids:
circuit_has_connections = True
if circuit_has_connections and hide_unconnected:
if circuit_has_connections and not show_unconnected:
if circuit_termination.circuit_id not in nodes_circuits:
nodes_circuits[
circuit_termination.circuit.pk
@@ -345,15 +361,6 @@ def get_topology_data(
for d in nodes_circuits.values():
nodes.append(create_node(d, save_coords))
links: QuerySet[CableTermination] = CableTermination.objects.filter(
Q(_device_id__in=device_ids)
).select_related("termination_type")
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
Q(_interface_a_device_id__in=device_ids)
& Q(_interface_b_device_id__in=device_ids)
)
if show_power:
power_panels_ids = PowerPanel.objects.filter(
Q(site_id__in=site_ids)
@@ -363,15 +370,15 @@ def get_topology_data(
)
for power_feed in power_feeds:
if not hide_unconnected or (
hide_unconnected and power_feed.cable_id is not None
if show_unconnected or (
not show_unconnected and power_feed.cable_id is not None
):
if power_feed.power_panel_id not in nodes_powerpanel:
nodes_powerpanel[power_feed.power_panel.pk] = power_feed.power_panel
power_link_name = ""
if power_feed.pk not in nodes_powerfeed:
if hide_unconnected:
if not show_unconnected:
if power_feed.link_peers[0].device_id in device_ids:
nodes_powerfeed[power_feed.pk] = power_feed
power_link_name = power_feed.link_peers[0].name
@@ -407,114 +414,156 @@ def get_topology_data(
for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords))
for link in links:
if link.termination_type.name in ignore_cable_type:
continue
# Normal device cables
if link.termination_type.name in supported_termination_types:
complete_link = False
if link.cable_end == "A":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "B" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["B"] is not None:
complete_link = True
elif link.cable_end == "B":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "A" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["A"] is not None:
complete_link = True
else:
print("Unkown cable end")
cable_ids[link.cable_id][link.cable_end] = link
if complete_link:
edge_ids += 1
if isinstance(cable_ids[link.cable_id]["B"], CableTermination):
if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices:
nodes_devices[
cable_ids[link.cable_id]["B"]._device_id
] = cable_ids[link.cable_id]["B"].termination.device
termination_b = {
"termination_name": cable_ids[link.cable_id][
"B"
].termination.name,
"termination_device_name": cable_ids[link.cable_id][
"B"
].termination.device.name,
"device_id": cable_ids[link.cable_id][
"B"
].termination.device_id,
}
else:
termination_b = cable_ids[link.cable_id]["B"]
if isinstance(cable_ids[link.cable_id]["A"], CableTermination):
if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices:
nodes_devices[
cable_ids[link.cable_id]["A"]._device_id
] = cable_ids[link.cable_id]["A"].termination.device
termination_a = {
"termination_name": cable_ids[link.cable_id][
"A"
].termination.name,
"termination_device_name": cable_ids[link.cable_id][
"A"
].termination.device.name,
"device_id": cable_ids[link.cable_id][
"A"
].termination.device_id,
}
else:
termination_a = cable_ids[link.cable_id]["A"]
edges.append(
create_edge(
edge_id=edge_ids,
cable=link.cable,
termination_a=termination_a,
termination_b=termination_b,
)
)
for wlan_link in wlan_links:
if wlan_link.interface_a.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_a.device.pk
] = wlan_link.interface_a.device
if wlan_link.interface_b.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_b.device.pk
] = wlan_link.interface_b.device
termination_a = {
"termination_name": wlan_link.interface_a.name,
"termination_device_name": wlan_link.interface_a.device.name,
"device_id": wlan_link.interface_a.device_id,
}
termination_b = {
"termination_name": wlan_link.interface_b.name,
"termination_device_name": wlan_link.interface_b.device.name,
"device_id": wlan_link.interface_b.device_id,
}
wireless = {"ssid": wlan_link.ssid}
edge_ids += 1
edges.append(
create_edge(
edge_id=edge_ids,
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
)
if show_logical_connections:
interfaces = Interface.objects.filter(
Q(_path__is_complete=True) & Q(device_id__in=device_ids)
)
for interface in interfaces:
# print('{} {} {} {}'.format(interface.device.name, interface.name, interface._path.destinations[0].device.name, interface._path.destinations[0].name))
for destination in interface._path.destinations:
if isinstance(destination, device_components.Interface):
if destination.device.id not in device_ids:
# print('Destination interface not in device queryset, ignoring')
continue
if destination.id in interface_ids:
# we've already captured the destination interface, ignore this connection
# print('Destination interface already exists, ignoring')
continue
if not show_single_cable_logical_conns and interface.cable_id==destination.cable_id and show_cables:
# interface connection is the same as the cable connection, ignore this connection
continue
interface_ids[interface.id]=interface
edge_ids += 1
termination_a = { "termination_name": interface.name, "termination_device_name": interface.device.name, "device_id": interface.device.id }
termination_b = { "termination_name": destination.name, "termination_device_name": destination.device.name, "device_id": destination.device.id }
edges.append(create_edge(edge_id=edge_ids, termination_a=termination_a, termination_b=termination_b, interface=interface))
nodes_devices[interface.device.id] = interface.device
nodes_devices[destination.device.id] = destination.device
if show_cables:
links: QuerySet[CableTermination] = CableTermination.objects.filter(
Q(_device_id__in=device_ids)
).select_related("termination_type")
for link in links:
if link.termination_type.name in ignore_cable_type:
continue
# Normal device cables
if link.termination_type.name in supported_termination_types:
complete_link = False
if link.cable_end == "A":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "B" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["B"] is not None:
complete_link = True
elif link.cable_end == "B":
if link.cable_id not in cable_ids:
cable_ids[link.cable_id] = {}
else:
if "A" in cable_ids[link.cable_id]:
if cable_ids[link.cable_id]["A"] is not None:
complete_link = True
else:
print("Unkown cable end")
cable_ids[link.cable_id][link.cable_end] = link
if complete_link:
edge_ids += 1
if isinstance(cable_ids[link.cable_id]["B"], CableTermination):
if cable_ids[link.cable_id]["B"]._device_id not in nodes_devices:
nodes_devices[
cable_ids[link.cable_id]["B"]._device_id
] = cable_ids[link.cable_id]["B"].termination.device
termination_b = {
"termination_name": cable_ids[link.cable_id][
"B"
].termination.name,
"termination_device_name": cable_ids[link.cable_id][
"B"
].termination.device.name,
"device_id": cable_ids[link.cable_id][
"B"
].termination.device_id,
}
else:
termination_b = cable_ids[link.cable_id]["B"]
if isinstance(cable_ids[link.cable_id]["A"], CableTermination):
if cable_ids[link.cable_id]["A"]._device_id not in nodes_devices:
nodes_devices[
cable_ids[link.cable_id]["A"]._device_id
] = cable_ids[link.cable_id]["A"].termination.device
termination_a = {
"termination_name": cable_ids[link.cable_id][
"A"
].termination.name,
"termination_device_name": cable_ids[link.cable_id][
"A"
].termination.device.name,
"device_id": cable_ids[link.cable_id][
"A"
].termination.device_id,
}
else:
termination_a = cable_ids[link.cable_id]["A"]
edges.append(
create_edge(
edge_id=edge_ids,
cable=link.cable,
termination_a=termination_a,
termination_b=termination_b,
)
)
if show_wireless:
wlan_links: QuerySet[WirelessLink] = WirelessLink.objects.filter(
Q(_interface_a_device_id__in=device_ids)
& Q(_interface_b_device_id__in=device_ids)
)
for wlan_link in wlan_links:
if wlan_link.interface_a.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_a.device.pk
] = wlan_link.interface_a.device
if wlan_link.interface_b.device_id not in nodes_devices:
nodes_devices[
wlan_link.interface_b.device.pk
] = wlan_link.interface_b.device
termination_a = {
"termination_name": wlan_link.interface_a.name,
"termination_device_name": wlan_link.interface_a.device.name,
"device_id": wlan_link.interface_a.device_id,
}
termination_b = {
"termination_name": wlan_link.interface_b.name,
"termination_device_name": wlan_link.interface_b.device.name,
"device_id": wlan_link.interface_b.device_id,
}
wireless = {"ssid": wlan_link.ssid}
edge_ids += 1
edges.append(
create_edge(
edge_id=edge_ids,
cable=wlan_link,
termination_a=termination_a,
termination_b=termination_b,
wireless=wireless,
)
)
for qs_device in queryset:
if qs_device.pk not in nodes_devices and not hide_unconnected:
if qs_device.pk not in nodes_devices and show_unconnected:
nodes_devices[qs_device.pk] = qs_device
results = {}
@@ -540,18 +589,30 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"device_type", "device_role"
)
self.queryset = self.filterset(request.GET, self.queryset).qs
self.model = self.queryset.model
topo_data = None
individualOptions, created = IndividualOptions.objects.get_or_create(
user_id=request.user.id,
)
if request.GET:
save_coords = False
if "save_coords" in request.GET:
if request.GET["save_coords"] == "on":
save_coords = True
# General options overrides
if save_coords == True and settings.PLUGINS_CONFIG["netbox_topology_views"]["allow_coordinates_saving"] == False:
save_coords = False
messages.warning(request, "Coordinate saving not allowed. Setting has been overridden")
elif settings.PLUGINS_CONFIG["netbox_topology_views"]["always_save_coordinates"] == True:
save_coords = True
hide_unconnected = False
if "hide_unconnected" in request.GET:
if request.GET["hide_unconnected"] == "on":
hide_unconnected = True
# Individual options
show_unconnected = False
if "show_unconnected" in request.GET:
if request.GET["show_unconnected"] == "on":
show_unconnected = True
show_power = False
if "show_power" in request.GET:
@@ -563,54 +624,65 @@ class TopologyHomeView(PermissionRequiredMixin, View):
if request.GET["show_circuit"] == "on":
show_circuit = True
if "draw_init" in request.GET:
if request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(
self.queryset,
hide_unconnected,
save_coords,
show_circuit,
show_power,
)
else:
show_logical_connections = False
if "show_logical_connections" in request.GET:
if request.GET["show_logical_connections"] == "on" :
show_logical_connections = True
show_single_cable_logical_conns = False
if "show_single_cable_logical_conns" in request.GET:
if request.GET["show_single_cable_logical_conns"] == "on" :
show_single_cable_logical_conns = True
show_cables = False
if "show_cables" in request.GET:
if request.GET["show_cables"] == "on" :
show_cables = True
show_wireless = False
if "show_wireless" in request.GET:
if request.GET["show_wireless"] == "on" :
show_wireless = True
if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "true":
topo_data = get_topology_data(
self.queryset,
hide_unconnected,
individualOptions,
show_unconnected,
save_coords,
show_cables,
show_circuit,
show_logical_connections,
show_single_cable_logical_conns,
show_power,
show_wireless,
)
else:
preselected_device_roles = settings.PLUGINS_CONFIG["netbox_topology_views"][
"preselected_device_roles"
]
preselected_tags = settings.PLUGINS_CONFIG["netbox_topology_views"][
"preselected_tags"
]
always_save_coordinates = bool(
settings.PLUGINS_CONFIG["netbox_topology_views"][
"always_save_coordinates"
]
)
q_device_role_id = DeviceRole.objects.filter(
name__in=preselected_device_roles
).values_list("id", flat=True)
q_tags = Tag.objects.filter(name__in=preselected_tags).values_list(
"name", flat=True
)
# No GET-Request in URL. We most likely came here from the navigation menu.
preselected_device_roles = IndividualOptions.objects.get(id=individualOptions.id).preselected_device_roles.all().values_list('id', flat=True)
preselected_tags = IndividualOptions.objects.get(id=individualOptions.id).preselected_tags.all().values_list(Lower('name'), flat=True)
q = QueryDict(mutable=True)
q.setlist("device_role_id", list(q_device_role_id))
q.setlist("tag", list(q_tags))
q["draw_init"] = settings.PLUGINS_CONFIG["netbox_topology_views"][
"draw_default_layout"
]
if always_save_coordinates:
q["save_coords"] = "on"
q.setlist("device_role_id", list(preselected_device_roles))
q.setlist("tag", list(preselected_tags))
if individualOptions.show_unconnected: q['show_unconnected'] = "on"
if individualOptions.show_cables: q['show_cables'] = "on"
if individualOptions.show_logical_connections: q['show_logical_connections'] = "on"
if individualOptions.show_single_cable_logical_conns: q['show_single_cable_logical_conns'] = "on"
if individualOptions.show_circuit: q['show_circuit'] = "on"
if individualOptions.show_power: q['show_power'] = "on"
if individualOptions.show_wireless: q['show_wireless'] = "on"
if individualOptions.draw_default_layout:
q['draw_init'] = "true"
else:
q['draw_init'] = "false"
query_string = q.urlencode()
return HttpResponseRedirect(f"{request.path}?{query_string}")
if is_htmx(request):
return render(
request,
@@ -630,6 +702,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
"filter_form": DeviceFilterForm(request.GET, label_suffix=""),
"topology_data": json.dumps(topo_data),
"broken_image": find_image_url("role-unknown"),
"model": self.model,
},
)
@@ -690,3 +763,48 @@ class TopologyImagesView(PermissionRequiredMixin, View):
"images": images,
},
)
class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
permission_required = 'netbox_topology_views.change_individualoptions'
def post(self, request):
instance = IndividualOptions.objects.get(user_id=request.user.id)
form = IndividualOptionsForm(request.POST, instance=instance)
if form.is_valid():
form.save()
messages.success(request, "Options have been sucessfully saved")
else:
messages.error(request, form.errors)
return HttpResponseRedirect("./")
def get(self, request):
queryset, created = IndividualOptions.objects.get_or_create(
user_id=request.user.id,
)
form = IndividualOptionsForm(
initial={
'user_id': request.user.id,
'ignore_cable_type': tuple(queryset.ignore_cable_type.translate({ord(i): None for i in '[]\''}).split(', ')),
'preselected_device_roles': IndividualOptions.objects.get(id=queryset.id).preselected_device_roles.all(),
'preselected_tags': IndividualOptions.objects.get(id=queryset.id).preselected_tags.all(),
'show_unconnected': queryset.show_unconnected,
'show_cables': queryset.show_cables,
'show_logical_connections': queryset.show_logical_connections,
'show_single_cable_logical_conns': queryset.show_single_cable_logical_conns,
'show_circuit': queryset.show_circuit,
'show_power': queryset.show_power,
'show_wireless': queryset.show_wireless,
'draw_default_layout': queryset.draw_default_layout,
},
)
return render(
request,
"netbox_topology_views/individual_options.html",
{
"form": form,
"object": queryset,
},
)
+1 -1
View File
@@ -7,7 +7,7 @@ long_description = readme.read_text()
setup(
name="netbox-topology-views",
version="3.1.1",
version="3.3.0",
description="An NetBox plugin to create Topology maps",
long_description=long_description,
long_description_content_type="text/markdown",