Merge pull request #297 from dreng/263_add_multiple_coordinates

Closes #263, Fixes #294: Multiple coordinates
This commit is contained in:
Mario
2023-05-23 18:20:19 +02:00
committed by GitHub
33 changed files with 1474 additions and 78 deletions
+66 -22
View File
@@ -2,14 +2,15 @@
![Version](https://img.shields.io/pypi/v/netbox-topology-views) ![Downloads](https://img.shields.io/pypi/dm/netbox-topology-views) ![Version](https://img.shields.io/pypi/v/netbox-topology-views) ![Downloads](https://img.shields.io/pypi/dm/netbox-topology-views)
Create topology views/maps from your devices in netbox. Create topology views/maps from your devices in NetBox.
The connections are based on the cables you created in netbox. The connections are based on the cables you created in NetBox.
Support to filter on name, site, tag and device role. Support to filter on name, site, tag and device role.
Options to export to xml (for draw.io/diagrams.net) or png. Options to export to xml (for draw.io/diagrams.net) or png.
## Preview ## Preview
![preview image](doc/img/preview_3.1.jpeg?raw=true "preview") ![Topology with light mode](doc/img/topology_light.png)
![Topology with dark mode](doc/img/topology_dark.png)
## Install ## Install
@@ -58,21 +59,24 @@ python3 manage.py collectstatic --no-input
| >= 2.10.0 | >= v0.5.0 | | >= 2.10.0 | >= v0.5.0 |
| < 2.10.0 | =< v0.4.10 | | < 2.10.0 | =< v0.4.10 |
### Custom field: coordinates ### Update
There is also support for custom fields. Run `pip install netbox-topology-views --upgrade` in your venv.
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. Run `python3 manage.py migrate netbox_topology_views`
The coordinates are stored as: "X;Y". Run `python3 manage.py collectstatic --no-input`
Please read the "Configure" chapter to set the `allow_coordinates_saving` option to True. Clear you browser cache.
You might also set the `always_save_coordinates` option to True.
## Configure ## Configure
### Individual Options
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. 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.
![Individual Options](doc/img/topology_individual_options.png)
The remaining options must be configured in the `PLUGINS_CONFIG` section of your `netbox/configuration.py`. The remaining options must be configured in the `PLUGINS_CONFIG` section of your `netbox/configuration.py`.
Example: Example:
@@ -89,12 +93,40 @@ PLUGINS_CONFIG = {
| Setting | Default value | Description | | 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` | | 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` |
| allow_coordinates_saving | False | (bool) Set to true if you use the custom coordinates fields and want to save the coordinates | | allow_coordinates_saving | False | (bool) Set to true if you want to enable the ability to save the coordinates. |
| always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default | | always_save_coordinates | False | (bool) Set if you want to enable the option to save coordinates by default. Setting allow_coordinates_saving to true is mandatory. |
### Custom field: coordinates
There is also support for custom fields.
>**_Note:_** The custom field "coordinates" is deprecated and will be removed in the future. Please use Coordinate Groups instead.
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.
### Convert custom field to Coordinate Groups
Please note that values stored in the custom field "coordinates" are not being converted to Coordinate Groups automatically. A pragmatic way to do this conversion yourself is as follows:
+ Navigate to "Topology" > "Filters".
+ Select "Show Unconnected" and "Show Cables".
+ Leave all other filter settings alone. We want all entries to be displayed!
+ Click "Search" and wait for the results to be displayed
+ Select all nodes. This can be done by holding down the Shift key and dragging a frame around all icons with the left mouse button.
+ Drag the selection a tiny bit to the side. This causes all coordinates for all devices to be stored in the "default" group.
> **_Hint_**: Don't wait too long after clicking an icon in order to drag. If you hold the mouse button for too long before dragging starts, the selection is reset._
+ Storing the values might take some time, depending on the number of devices. Please be patient and check "Coordinates" in order to make sure that everthing has been stored.
+ It is save to delete the custom field now.
### Custom Images ### 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. 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.
![Images](doc/img/topology_images.png)
## Use ## Use
@@ -102,9 +134,11 @@ Go to the plugins tab in the navbar and click topology or go to `$NETBOX_URL/plu
Select your options for the topology view: Select your options for the topology view:
![topology_options](https://user-images.githubusercontent.com/20901110/235302073-234c3ef7-2c2b-4d0c-a674-9909d490d7ea.png) ![Filter Options](doc/img/topology_filter_options.png)
<dl> <dl>
<dt>Coordinate Group</dt>
<dd>Select Coordinate Group. These groups allow devices to be displayed in different positions depending on the group, thus providing different representations for the same topology. If nothing is selected, the group "default" is set automatically.</dd>
<dt>Save Coordinates</dt> <dt>Save Coordinates</dt>
<dd>Save the coordinates of devices in the topology view.</dd> <dd>Save the coordinates of devices in the topology view.</dd>
<dt>Show Unconnected</dt> <dt>Show Unconnected</dt>
@@ -127,17 +161,18 @@ Select your options for the topology view:
<dd>Displays wireless connections. These connections are displayed as blue dotted lines.</dd> <dd>Displays wireless connections. These connections are displayed as blue dotted lines.</dd>
</dl> </dl>
### Update ### Coordinates and Coordinate Groups
Run `pip install netbox-topology-views --upgrade` in your venv. Netbox Topology Views stores the position of the devices. In order to allow different representations for the topology, Coordinate Groups are supported.
Run `python3 manage.py migrate netbox_topology_views` > 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.
Run `python3 manage.py collectstatic --no-input` Navigate to "Coordinate Groups" in the menu and create as many groups as you like. You can select a group later in the filter pane in order to show icon positions according to this group (see chapter "Use"). You can also omit creating a group if you don't need this feature. Netbox Topology Views automatically creates a group named "default" for you and stores all coordinates in this group, even if you do not select a group in the filter.
By default, the position of the devices are calculated with a physics engine. As soon as a device icon is dragged to another location, its position is saved and excluded from the calculation by the physics engine. All saved coordinates can be viewed and edited under the menu item "Coordinates".
Clear you browser cache. > **_Note:_** At the time of writing, it is not possible to store the positions of circuit terminations, power panels and power feeds, as these are not devices.
### Permissions ### Permissions
@@ -147,11 +182,20 @@ To view `/plugins/netbox_topology-views/topology` you need the following permiss
+ extras | tag | can view tag + extras | tag | can view tag
+ dcim | device role | can view device role + dcim | device role | can view device role
To view `/plugins/netbox_topology-views/image`: To save `Coordinates` when moving icons:
+ netbox_topology_views | coordinate | change
To view `/plugins/netbox_topology-views/images`:
+ dcim | site | view + dcim | site | view
+ dcim | device role | view + dcim | device role | view
+ dcim | device role | add + dcim | device role | add
+ dcim | device role | change + dcim | device role | change
To view `/plugins/netbox_topology-views/individualoptions`: To view `/plugins/netbox_topology-views/individualoptions`:
+ netbox topology views | individualoptions | change + netbox_topology_views | individual options | change
Set `Coordinate Groups` according to your needs:
+ netbox_topology_views | coordinate groups | view/add/change/delete
Set `Coordinates` according to your needs:
+ netbox_topology_views | coordinate | view/add/change/delete
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

+11 -1
View File
@@ -2,7 +2,7 @@ from dcim.models import Device, DeviceRole
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from netbox.api.serializers import NetBoxModelSerializer from netbox.api.serializers import NetBoxModelSerializer
from netbox_topology_views.models import RoleImage, IndividualOptions from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate
class TopologyDummySerializer(ModelSerializer): class TopologyDummySerializer(ModelSerializer):
@@ -22,6 +22,16 @@ class DeviceRoleSerializer(ModelSerializer):
model = DeviceRole model = DeviceRole
fields = ("name", "slug", "color", "vm_role", "description") fields = ("name", "slug", "color", "vm_role", "description")
class CoordinateGroupSerializer(NetBoxModelSerializer):
class Meta:
model = CoordinateGroup
fields = ("name", "description")
class CoordinateSerializer(NetBoxModelSerializer):
class Meta:
model = Coordinate
fields = ("x", "y")
class IndividualOptionsSerializer(NetBoxModelSerializer): class IndividualOptionsSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = IndividualOptions model = IndividualOptions
+32 -9
View File
@@ -14,12 +14,14 @@ from netbox_topology_views.api.serializers import (
RoleImageSerializer, RoleImageSerializer,
TopologyDummySerializer, TopologyDummySerializer,
) )
from netbox_topology_views.models import RoleImage, IndividualOptions from netbox_topology_views.models import RoleImage, IndividualOptions, CoordinateGroup, Coordinate
from netbox_topology_views.views import get_topology_data from netbox_topology_views.views import get_topology_data
from netbox_topology_views.utils import get_image_from_url, export_data_to_xml, get_query_settings from netbox_topology_views.utils import get_image_from_url, export_data_to_xml, get_query_settings
from netbox_topology_views.filters import DeviceFilterSet from netbox_topology_views.filters import DeviceFilterSet
class SaveCoordsViewSet(ReadOnlyModelViewSet): class SaveCoordsViewSet(PermissionRequiredMixin, ReadOnlyModelViewSet):
permission_required = 'netbox_topology_views.change_coordinate'
queryset = Device.objects.none() queryset = Device.objects.none()
serializer_class = TopologyDummySerializer serializer_class = TopologyDummySerializer
@@ -33,6 +35,7 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
device_id: str = request.data.get("node_id", None) device_id: str = request.data.get("node_id", None)
x_coord = request.data.get("x", None) x_coord = request.data.get("x", None)
y_coord = request.data.get("y", None) y_coord = request.data.get("y", None)
group_id = request.data.get("group", "None")
actual_device = None actual_device = None
if device_id.startswith("c"): if device_id.startswith("c"):
@@ -50,22 +53,37 @@ class SaveCoordsViewSet(ReadOnlyModelViewSet):
if not actual_device: if not actual_device:
return Response({"status": "invalid node_id in body"}, status=400) return Response({"status": "invalid node_id in body"}, status=400)
if group_id is None or group_id == "default":
group_id = Coordinate.get_or_create_default_group(group_id)
if not group_id:
return Response(
{"status": "Error while creating default group."}, status=500
)
try: try:
actual_device.custom_field_data["coordinates"] = "%s;%s" % ( if CoordinateGroup.objects.filter(pk=group_id):
x_coord, group = CoordinateGroup.objects.get(pk=group_id)
y_coord, # Hen-and-egg-problem. Thanks, Django! By default, Django updates records that
) # already exist and inserts otherwise. This does not work with our
actual_device.save() # unique_together key if no pk is given. But: No record, no pk.
if not Coordinate.objects.filter(group=group, device=actual_device):
# Unique group/device pair does not exist. Prepare new data set
coords = Coordinate(group=group, device=actual_device, x=x_coord, y=y_coord)
else:
# Unique group/device pair already exists. Update data
coords = Coordinate(pk=Coordinate.objects.get(group=group, device=actual_device).pk, group=group, device=actual_device, x=x_coord, y=y_coord)
coords.save()
except: except:
return Response( return Response(
{"status": "coords custom field could not be saved"}, status=500 {"status": "Coordinates could not be saved."}, status=500
) )
return Response({"status": "saved coords"}) return Response({"status": "saved coords"})
class ExportTopoToXML(PermissionRequiredMixin, ViewSet): class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
queryset = Device.objects.none()
permission_required = ("dcim.view_site", "dcim.view_device") permission_required = ("dcim.view_site", "dcim.view_device")
queryset = Device.objects.none()
serializer_class = TopologyDummySerializer serializer_class = TopologyDummySerializer
def list(self, request): def list(self, request):
@@ -83,6 +101,10 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
if request.GET: if request.GET:
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request) save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
if 'group' not in request.query_params:
group_id = "default"
else:
group_id = request.query_params["group"]
topo_data = get_topology_data( topo_data = get_topology_data(
queryset=self.queryset, queryset=self.queryset,
individualOptions=individualOptions, individualOptions=individualOptions,
@@ -95,6 +117,7 @@ class ExportTopoToXML(PermissionRequiredMixin, ViewSet):
show_circuit=show_circuit, show_circuit=show_circuit,
show_power=show_power, show_power=show_power,
show_wireless=show_wireless, show_wireless=show_wireless,
group_id=group_id,
) )
xml_data = export_data_to_xml(topo_data).decode('utf-8') xml_data = export_data_to_xml(topo_data).decode('utf-8')
+23 -1
View File
@@ -5,7 +5,7 @@ from django.db.models import Q
from netbox.filtersets import NetBoxModelFilterSet from netbox.filtersets import NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet from tenancy.filtersets import TenancyFilterSet
from utilities.filters import TreeNodeMultipleChoiceFilter from utilities.filters import TreeNodeMultipleChoiceFilter
from netbox_topology_views.models import CoordinateGroup, Coordinate
class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet): class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
@@ -53,3 +53,25 @@ class DeviceFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
return queryset return queryset
qs_filter = Q(name__icontains=value) qs_filter = Q(name__icontains=value)
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class CoordinatesFilterSet(NetBoxModelFilterSet):
group = django_filters.ModelMultipleChoiceFilter(
queryset = CoordinateGroup.objects.all(),
)
device = django_filters.ModelMultipleChoiceFilter(
queryset = Device.objects.all(),
)
class Meta:
model = Coordinate
fields = ['id', 'group', 'device', 'x', 'y']
def search(self, queryset, name, value):
"""Perform the filtered search."""
if not value.strip():
return queryset
return queryset.filter(
Q(group__name__icontains=value) |
Q(device__name__icontains=value)
)
+66 -4
View File
@@ -12,15 +12,14 @@ from dcim.choices import DeviceStatusChoices
from tenancy.models import TenantGroup, Tenant from tenancy.models import TenantGroup, Tenant
from tenancy.forms import TenancyFilterForm from tenancy.forms import TenancyFilterForm
from django.conf import settings from django.conf import settings
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm, NetBoxModelImportForm
from utilities.forms.fields import ( from utilities.forms.fields import (
TagFilterField, TagFilterField,
DynamicModelMultipleChoiceField, DynamicModelMultipleChoiceField,
MultipleChoiceField MultipleChoiceField
) )
from netbox_topology_views.models import IndividualOptions, CoordinateGroup, Coordinate
from .models import IndividualOptions
class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Device model = Device
@@ -30,6 +29,12 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
( (
"q", "q",
"filter_id", "filter_id",
),
),
(
None,
(
"group",
"save_coords", "save_coords",
"show_unconnected", "show_unconnected",
"show_cables", "show_cables",
@@ -59,7 +64,11 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
), ),
(None, ("tag",)), (None, ("tag",)),
) )
group = forms.ModelChoiceField(
queryset=CoordinateGroup.objects.all(),
required=False,
label=_("Coordinate group"),
)
region_id = DynamicModelMultipleChoiceField( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), required=False, label=_("Region") queryset=Region.objects.all(), required=False, label=_("Region")
) )
@@ -141,6 +150,59 @@ class DeviceFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label =_("Show Wireless Links"), required=False, initial=False label =_("Show Wireless Links"), required=False, initial=False
) )
class CoordinateGroupsForm(NetBoxModelForm):
fieldsets = (
('Group Details', ('name', 'description')),
)
class Meta:
model = CoordinateGroup
fields = ('name', 'description')
class CoordinateGroupsImportForm(NetBoxModelImportForm):
class Meta:
model = CoordinateGroup
fields = ('name', 'description')
class CoordinatesForm(NetBoxModelForm):
fieldsets = (
('Coordinate', ('group', 'device', 'x', 'y')),
)
class Meta:
model = Coordinate
fields = ('group', 'device', 'x', 'y')
class CoordinatesImportForm(NetBoxModelImportForm):
class Meta:
model = Coordinate
fields = ('group', 'device', 'x', 'y')
class CoordinatesFilterForm(NetBoxModelFilterSetForm):
model = Coordinate
fieldsets = (
(None, ('q', 'filter_id')),
('Coordinates', ('group', 'device', 'x', 'y'))
)
group = forms.ModelMultipleChoiceField(
queryset=CoordinateGroup.objects.all(),
required=False
)
device = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
required=False
)
x = forms.IntegerField(
required=False
)
y = forms.IntegerField(
required=False
)
class IndividualOptionsForm(NetBoxModelForm): class IndividualOptionsForm(NetBoxModelForm):
fieldsets = ( fieldsets = (
( (
@@ -0,0 +1,51 @@
# Generated by Django 4.1.8 on 2023-05-01 16:13
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('dcim', '0171_cabletermination_change_logging'),
('extras', '0092_delete_jobresult'),
('netbox_topology_views', '0003_individualoptions_show_neighbors'),
]
operations = [
migrations.CreateModel(
name='CoordinateGroup',
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)),
('name', models.CharField(max_length=100, unique=True)),
('description', models.CharField(blank=True, max_length=255)),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Coordinate',
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)),
('x', models.IntegerField()),
('y', models.IntegerField()),
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dcim.device')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='netbox_topology_views.coordinategroup')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
],
options={
'ordering': ['group', 'device'],
'unique_together': {('device', 'group')},
},
),
]
+76 -2
View File
@@ -1,12 +1,13 @@
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from dcim.models import DeviceRole from dcim.models import Device, DeviceRole
from extras.models import Tag from extras.models import Tag
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.templatetags.static import static from django.templatetags.static import static
from django.urls import reverse
from netbox.models import NetBoxModel from netbox.models import NetBoxModel
from netbox.models.features import ( from netbox.models.features import (
ChangeLoggingMixin, ChangeLoggingMixin,
@@ -94,6 +95,79 @@ class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
return self.get_default_image(dir) return self.get_default_image(dir)
return static(f"/{self.image}") return static(f"/{self.image}")
class CoordinateGroup(NetBoxModel):
"""
A coordinate group is used to display the topology for a particular group.
This allows different visualizations with the same devices.
"""
name = models.CharField(
max_length=100,
unique=True,
)
description = models.CharField(
max_length=255,
blank = True,
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:coordinategroup', args=[self.pk])
class Coordinate(NetBoxModel):
"""
Coordinates are being used to place devices in a topology view onto a certain
position. Devices belong to one or more coordinate groups. They have to
be unique together.
"""
device = models.ForeignKey(Device, on_delete=models.CASCADE)
group = models.ForeignKey(CoordinateGroup, on_delete=models.CASCADE)
x = models.IntegerField(
help_text='X-coordinate of the device (horizontal) on the canvas. '
'Smaller values correspond to a position further up on the monitor.',
)
y = models.IntegerField(
help_text='Y-coordinate of the device (vertical) on the canvas. '
'Smaller values correspond to a position further to the left on the monitor.',
)
def get_or_create_default_group(group_id):
# Default group named "default" must always exist in order to make sure
# that coordinate values can be stored even if no coordinate group has been
# selected. The default group will be added automatically if it does not exist.
try:
if CoordinateGroup.objects.filter(name="default"):
group = CoordinateGroup.objects.get(name="default")
group_id = group.pk
else:
group = CoordinateGroup(
name="default",
description="Automatically generated default group. If you delete "
"this group, all default coordinates are gone for good but "
"the group itself will be re-created."
)
group.save()
group_id = group.pk
except:
return False
return group_id
class Meta:
ordering = ['group', 'device']
unique_together = ('device', 'group')
def __str__(self):
return f'{self.x};{self.y}'
def get_absolute_url(self):
return reverse('plugins:netbox_topology_views:coordinate', args=[self.pk])
class IndividualOptions(NetBoxModel): class IndividualOptions(NetBoxModel):
CHOICES = ( CHOICES = (
('interface', 'interface'), ('interface', 'interface'),
@@ -154,4 +228,4 @@ class IndividualOptions(NetBoxModel):
) )
def __str___(self): def __str___(self):
return f"{self.user_id}" return f"{self.user_id}"
+39 -2
View File
@@ -1,10 +1,47 @@
from extras.plugins import PluginMenu, PluginMenuItem from extras.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
from utilities.choices import ButtonColorChoices
coordinategroup_buttons = (
PluginMenuButton(
link='plugins:netbox_topology_views:coordinategroup_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
),
PluginMenuButton(
link='plugins:netbox_topology_views:coordinategroup_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
)
)
coordinate_buttons = (
PluginMenuButton(
link='plugins:netbox_topology_views:coordinate_add',
title='Add',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN,
),
PluginMenuButton(
link='plugins:netbox_topology_views:coordinate_import',
title='Import',
icon_class='mdi mdi-upload',
color=ButtonColorChoices.CYAN,
)
)
menu = PluginMenu( menu = PluginMenu(
label='Topology Views', label='Topology Views',
icon_class="mdi mdi-sitemap", icon_class="mdi mdi-sitemap",
groups=( groups=(
('TOPOLOGY', (PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),),), ('TOPOLOGY',
(
PluginMenuItem(link="plugins:netbox_topology_views:home", link_text="Topology"),
PluginMenuItem(link="plugins:netbox_topology_views:coordinategroup_list", link_text="Coordinate Groups", buttons=coordinategroup_buttons),
PluginMenuItem(link="plugins:netbox_topology_views:coordinate_list", link_text="Coordinates", buttons=coordinate_buttons),
),
),
('PREFERENCES', ('PREFERENCES',
( (
PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"), PluginMenuItem(link="plugins:netbox_topology_views:images", link_text="Images"),
+18
View File
@@ -0,0 +1,18 @@
from netbox.search import SearchIndex, register_search
from .models import CoordinateGroup, Coordinate
@register_search
class CoordinateGroupsIndex(SearchIndex):
model = CoordinateGroup
fields = (
('name', 100),
('description', 2000),
)
@register_search
class CoordinatesIndex(SearchIndex):
model = Coordinate
fields = (
('group', 100),
('device', 200),
)
File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -60,6 +60,9 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
})) }))
) )
// make nodes object available globally in order to update their physics and positions later
window.nodes = nodes;
const edges = new DataSet( const edges = new DataSet(
topologyData.edges.map((node) => ({ topologyData.edges.map((node) => ({
...node, ...node,
@@ -72,10 +75,11 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
graph.on('dragEnd', (params) => { graph.on('dragEnd', (params) => {
if (coordSaveCheckbox == null) return if (coordSaveCheckbox == null) return
if (!coordSaveCheckbox.checked) return if (!coordSaveCheckbox.checked) return
Promise.allSettled( Promise.allSettled(
Object.entries(graph.getPositions(params.nodes)).map( Object.entries(graph.getPositions(params.nodes)).map(
async ([nodeId, nodePosition]) => { async ([nodeId, nodePosition]) => {
window.nodes.update({id: parseInt(nodeId), physics: false, x: nodePosition.x, y: nodePosition.y});
const res = await fetch( const res = await fetch(
'/api/plugins/netbox_topology_views/save-coords/save_coords/', '/api/plugins/netbox_topology_views/save-coords/save_coords/',
{ {
@@ -88,7 +92,8 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
body: JSON.stringify({ body: JSON.stringify({
node_id: nodeId, node_id: nodeId,
x: nodePosition.x, x: nodePosition.x,
y: nodePosition.y y: nodePosition.y,
group: topologyData.group
}) })
} }
) )
+764 -1
View File
@@ -1,8 +1,771 @@
{ {
"name": "netbox_topology_views", "name": "netbox_topology_views",
"version": "3.5.0", "version": "3.5.0",
"lockfileVersion": 1, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {
"": {
"name": "netbox_topology_views",
"version": "3.5.0",
"dependencies": {
"vis-data": "^7.1.6",
"vis-network": "^9.1.6",
"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==",
"dev": true,
"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==",
"dev": true
},
"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,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.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==",
"dev": true
},
"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,
"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,
"dependencies": {
"@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"
},
"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,
"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"
}
},
"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==",
"dev": true
},
"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"
}
},
"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"
}
},
"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"
}
},
"node_modules/timsort": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
"dev": true
},
"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==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vis-data": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz",
"integrity": "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg=="
},
"node_modules/vis-network": {
"version": "9.1.6",
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz",
"integrity": "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g=="
},
"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"
}
}
},
"dependencies": { "dependencies": {
"@egjs/hammerjs": { "@egjs/hammerjs": {
"version": "2.0.17", "version": "2.0.17",
+8 -8
View File
@@ -363,15 +363,15 @@
"resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" "resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
"version" "8.3.2" "version" "8.3.2"
"vis-data@^7.1.4": "vis-data@^7.1.6":
"integrity" "sha512-usy+ePX1XnArNvJ5BavQod7YRuGQE1pjFl+pu7IS6rCom2EBoG0o1ZzCqf3l5US6MW51kYkLR+efxRbnjxNl7w==" "integrity" "sha512-lG7LJdkawlKSXsdcEkxe/zRDyW29a4r7N7PMwxCPxK12/QIdqxJwcMxwjVj9ozdisRhP5TyWDHZwsgjmj0g6Dg=="
"resolved" "https://registry.npmjs.org/vis-data/-/vis-data-7.1.4.tgz" "resolved" "https://registry.npmjs.org/vis-data/-/vis-data-7.1.6.tgz"
"version" "7.1.4" "version" "7.1.6"
"vis-network@^9.1.2": "vis-network@^9.1.6":
"integrity" "sha512-BdapguKg7sk3NvdZaDsM7T6rNhOBFz0/F4ZScxctK4klRzQPLQPTEcmbioXaZhMkkgWymzBR3lFCxL1q+eYyAw==" "integrity" "sha512-Eiwx1JleAsUqfy4pzcsFngCVlCEdjAtRPB/OwCV7PHBm+o2jtE4IZPcPITAEGUlxvL4Fdw7/lZsfD32dL+IL6g=="
"resolved" "https://registry.npmjs.org/vis-network/-/vis-network-9.1.2.tgz" "resolved" "https://registry.npmjs.org/vis-network/-/vis-network-9.1.6.tgz"
"version" "9.1.2" "version" "9.1.6"
"vis-util@^5.0.3": "vis-util@^5.0.3":
"integrity" "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g==" "integrity" "sha512-Wf9STUcFrDzK4/Zr7B6epW2Kvm3ORNWF+WiwEz2dpf5RdWkLUXFSbLcuB88n1W6tCdFwVN+v3V4/Xmn9PeL39g=="
+30
View File
@@ -0,0 +1,30 @@
import django_tables2 as tables
from netbox.tables import NetBoxTable, ChoiceFieldColumn
from netbox_topology_views.models import CoordinateGroup, Coordinate
class CoordinateGroupListTable(NetBoxTable):
name = tables.Column(
linkify=True
)
devices = tables.Column()
class Meta(NetBoxTable.Meta):
model = CoordinateGroup
fields = ('pk', 'id', 'name', 'description', 'devices')
default_columns = ('name', 'description', 'devices')
class CoordinateListTable(NetBoxTable):
group = tables.Column(
linkify=True
)
device = tables.Column(
linkify=True
)
class Meta(NetBoxTable.Meta):
model = Coordinate
fields = ('pk', 'id', 'group', 'device', 'x', 'y')
default_columns = ('id', 'group', 'device', 'x', 'y')
@@ -0,0 +1,42 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% block title %}Topology Views Coordinates{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinates
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Group</th>
<td>{{ object.group|linkify }}</td>
</tr>
<tr>
<th scope="row">Device</th>
<td>{{ object.device|linkify }}</td>
</tr>
<tr>
<th scope="row">X-Coordinate</th>
<td>{{ object.x }}</td>
</tr>
<tr>
<th scope="row">Y-Coordinate</th>
<td>{{ object.y }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add new Device Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Device Coordinates{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Device Coordinates{% endblock title %}
@@ -0,0 +1,45 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block title %}Topology Views Coordinate Group{% endblock title %}
{% block content %}
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Coordinate Group
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Coordinates</h5>
<div class="card-body table-responsive">
{% render_table coordinates_table %}
</div>
</div>
</div>
</div>
{% endblock content %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Add a new Coordinate Group{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_edit.html' %}
{% block title %}Edit Coordinate Group{% endblock title %}
@@ -0,0 +1,3 @@
{% extends 'generic/object_list.html' %}
{% block title %}Coordinate Groups{% endblock title %}
@@ -55,7 +55,6 @@
{# Applied filters #} {# Applied filters #}
{% if filter_form %} {% if filter_form %}
{% applied_filters model filter_form request.GET %} {% applied_filters model filter_form request.GET %}
<hr style="margin-top: 0px;">
{% endif %} {% endif %}
<div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab"> <div class="tab-pane show active" id="networks" role="tabpanel" aria-labelledby="network-tab">
+20 -2
View File
@@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from netbox.views.generic import ObjectChangeLogView
from . import views from . import models, views
# Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to # Define a list of URL patterns to be imported by NetBox. Each pattern maps a URL to
# a specific view so that it can be accessed by users. # a specific view so that it can be accessed by users.
@@ -10,4 +10,22 @@ urlpatterns = (
path("topology/", views.TopologyHomeView.as_view(), name="home"), path("topology/", views.TopologyHomeView.as_view(), name="home"),
path("images/", views.TopologyImagesView.as_view(), name="images"), path("images/", views.TopologyImagesView.as_view(), name="images"),
path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"), path("individualoptions/", views.TopologyIndividualOptionsView.as_view(), name="individualoptions"),
# Coordinate Group
path("coordinate-groups/", views.CoordinateGroupListView.as_view(), name="coordinategroup_list"),
path("coordinate-groups/add/", views.CoordinateGroupAddView.as_view(), name="coordinategroup_add"),
path('coordinate/import/', views.CoordinateGroupBulkImportView.as_view(), name='coordinategroup_import'),
path("coordinate-groups/<int:pk>/", views.CoordinateGroupView.as_view(), name="coordinategroup"),
path("coordinate-groups/<int:pk>/edit/", views.CoordinateGroupEditView.as_view(), name="coordinategroup_edit"),
path("coordinate-groups/<int:pk>/delete/", views.CoordinateGroupDeleteView.as_view(), name="coordinategroup_delete"),
path("coordinate-groups/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="coordinategroup_changelog", kwargs={'model': models.CoordinateGroup}),
# Coordinate
path("coordinate/", views.CoordinateListView.as_view(), name="coordinate_list"),
path("coordinate/add/", views.CoordinateAddView.as_view(), name="coordinate_add"),
path('coordinate/import/', views.CoordinateBulkImportView.as_view(), name='coordinate_import'),
path("coordinate/<int:pk>/", views.CoordinateView.as_view(), name="coordinate"),
path("coordinate/<int:pk>/edit/", views.CoordinateEditView.as_view(), name="coordinate_edit"),
path("coordinate/<int:pk>/delete/", views.CoordinateDeleteView.as_view(), name="coordinate_delete"),
path("coordinate/<int:pk>/changelog/", ObjectChangeLogView.as_view(), name="coordinate_changelog", kwargs={'model': models.Coordinate}),
) )
+157 -22
View File
@@ -22,17 +22,35 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db.models import Q, QuerySet from django.db.models import Q, QuerySet, Count
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.http import HttpRequest, HttpResponseRedirect, QueryDict from django.http import HttpRequest, HttpResponseRedirect, QueryDict
from django.shortcuts import render from django.shortcuts import render, get_object_or_404
from django.views.generic import View from django.views.generic import View
from extras.models import Tag from extras.models import Tag
from wireless.models import WirelessLink from wireless.models import WirelessLink
from netbox.views.generic import (
ObjectView,
ObjectListView,
ObjectEditView,
ObjectDeleteView,
ObjectChangeLogView,
BulkImportView
)
from netbox_topology_views.filters import DeviceFilterSet
from netbox_topology_views.forms import DeviceFilterForm, IndividualOptionsForm from netbox_topology_views.filters import DeviceFilterSet, CoordinatesFilterSet
from netbox_topology_views.models import RoleImage, IndividualOptions from netbox_topology_views.forms import (
DeviceFilterForm,
IndividualOptionsForm,
CoordinateGroupsForm,
CoordinatesForm,
CoordinatesFilterForm,
CoordinateGroupsImportForm,
CoordinatesImportForm
)
from netbox_topology_views.models import RoleImage, CoordinateGroup, Coordinate, IndividualOptions
from netbox_topology_views.tables import CoordinateGroupListTable, CoordinateListTable
from netbox_topology_views.utils import ( from netbox_topology_views.utils import (
CONF_IMAGE_DIR, CONF_IMAGE_DIR,
find_image_url, find_image_url,
@@ -62,7 +80,7 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]):
def create_node( def create_node(
device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, group_id="default"
): ):
node = {} node = {}
node_content = "" node_content = ""
@@ -144,8 +162,35 @@ def create_node(
if device.device_role.color != "": if device.device_role.color != "":
node["color.border"] = "#" + device.device_role.color node["color.border"] = "#" + device.device_role.color
dev_title = "<table><tbody> %s</tbody></table>" % (node_content) if group_id is None or group_id == "default":
group_id = Coordinate.get_or_create_default_group(group_id)
if not group_id:
print('Exception occured while handling default group.')
return node
group = get_object_or_404(CoordinateGroup, pk=group_id)
node["physics"] = True
# Coords must be set even if no coords have been stored. Otherwise nodes with coords
# will not be placed correctly by vis-network.
node["x"] = 0
node["y"] = 0
if Coordinate.objects.filter(group=group, device=device.pk).values('x') and Coordinate.objects.filter(group=group, device=device.pk).values('y'):
# Coordinates data for the device exists in Coordinates Group. Let's assign them
node["x"] = Coordinate.objects.get(group=group, device=device.pk).x
node["y"] = Coordinate.objects.get(group=group, device=device.pk).y
node["physics"] = False
elif "coordinates" in device.custom_field_data:
# We prefer the new Coordinate model but leave the deprecated method
# for now as fallback for compatibility reasons
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
dev_title = "<table><tbody> %s</tbody></table>" % (node_content)
node["title"] = dev_title node["title"] = dev_title
node["name"] = dev_name node["name"] = dev_name
node["label"] = dev_name node["label"] = dev_name
@@ -153,16 +198,6 @@ def create_node(
node["href"] = device.get_absolute_url() node["href"] = device.get_absolute_url()
node["image"] = get_image_for_entity(device) 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 return node
@@ -267,6 +302,7 @@ def get_topology_data(
show_neighbors: bool, show_neighbors: bool,
show_power: bool, show_power: bool,
show_wireless: bool, show_wireless: bool,
group_id,
): ):
supported_termination_types = [] supported_termination_types = []
@@ -306,7 +342,7 @@ def get_topology_data(
ports = chain(interfaces, frontports, rearports) ports = chain(interfaces, frontports, rearports)
for port in ports: for port in ports:
for link_peer in port.link_peers: for link_peer in port.link_peers:
if link_peer.device.id not in device_ids: if hasattr(link_peer, 'device') and link_peer.device.id not in device_ids:
device_ids.append(link_peer.device.id) device_ids.append(link_peer.device.id)
if show_logical_connections: if show_logical_connections:
@@ -388,7 +424,7 @@ def get_topology_data(
] = circuit_termination.circuit ] = circuit_termination.circuit
for d in nodes_circuits.values(): for d in nodes_circuits.values():
nodes.append(create_node(d, save_coords)) nodes.append(create_node(d, save_coords, group_id))
if show_power: if show_power:
power_panels_ids = PowerPanel.objects.filter( power_panels_ids = PowerPanel.objects.filter(
@@ -438,10 +474,10 @@ def get_topology_data(
cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b cable_ids[power_feed.cable_id][power_feed.cable_end] = termination_b
for d in nodes_powerfeed.values(): for d in nodes_powerfeed.values():
nodes.append(create_node(d, save_coords)) nodes.append(create_node(d, save_coords, group_id))
for d in nodes_powerpanel.values(): for d in nodes_powerpanel.values():
nodes.append(create_node(d, save_coords)) nodes.append(create_node(d, save_coords, group_id))
if show_logical_connections: if show_logical_connections:
interfaces = Interface.objects.filter( interfaces = Interface.objects.filter(
@@ -598,10 +634,11 @@ def get_topology_data(
results = {} results = {}
for d in nodes_devices.values(): for d in nodes_devices.values():
nodes.append(create_node(d, save_coords)) nodes.append(create_node(d, save_coords, group_id))
results["nodes"] = nodes results["nodes"] = nodes
results["edges"] = edges results["edges"] = edges
results["group"] = group_id
return results return results
@@ -629,6 +666,11 @@ class TopologyHomeView(PermissionRequiredMixin, View):
save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request) save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, show_neighbors = get_query_settings(request)
if "group" not in request.GET:
group_id = "default"
else:
group_id = request.GET["group"]
if not "draw_init" in request.GET or "draw_init" in request.GET and request.GET["draw_init"].lower() == "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( topo_data = get_topology_data(
queryset=self.queryset, queryset=self.queryset,
@@ -642,6 +684,7 @@ class TopologyHomeView(PermissionRequiredMixin, View):
show_circuit=show_circuit, show_circuit=show_circuit,
show_power=show_power, show_power=show_power,
show_wireless=show_wireless, show_wireless=show_wireless,
group_id=group_id,
) )
else: else:
@@ -751,6 +794,98 @@ class TopologyImagesView(PermissionRequiredMixin, View):
}, },
) )
class CoordinateView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = Coordinate.objects.all()
class CoordinateAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinate'
queryset = Coordinate.objects.all()
form = CoordinatesForm
template_name = 'netbox_topology_views/coordinate_add.html'
class CoordinateBulkImportView(BulkImportView):
queryset = Coordinate.objects.all()
model_form = CoordinatesImportForm
class CoordinateListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = Coordinate.objects.all()
table = CoordinateListTable
template_name = 'netbox_topology_views/coordinate_list.html'
filterset = CoordinatesFilterSet
filterset_form = CoordinatesFilterForm
class CoordinateEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinate'
queryset = Coordinate.objects.all()
form = CoordinatesForm
template_name = 'netbox_topology_views/coordinate_edit.html'
class CoordinateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinate'
queryset = Coordinate.objects.all()
class CoordinateChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinate'
queryset = Coordinate.objects.all()
class CoordinateGroupView(PermissionRequiredMixin, ObjectView):
permission_required = 'netbox_topology_views.view_coordinategroup'
queryset = CoordinateGroup.objects.all()
def get_extra_context(self, request, instance):
table = CoordinateListTable(instance.coordinate_set.all())
table.configure(request)
return {
'coordinates_table': table,
}
class CoordinateGroupAddView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.add_coordinategroup'
queryset = CoordinateGroup.objects.all()
form = CoordinateGroupsForm
template_name = 'netbox_topology_views/coordinategroup_add.html'
class CoordinateGroupBulkImportView(BulkImportView):
queryset = CoordinateGroup.objects.all()
model_form = CoordinateGroupsImportForm
class CoordinateGroupListView(PermissionRequiredMixin, ObjectListView):
permission_required = 'netbox_topology_views.view_coordinategroup'
queryset = CoordinateGroup.objects.annotate(
devices = Count('coordinate')
)
table = CoordinateGroupListTable
template_name = 'netbox_topology_views/coordinategroup_list.html'
class CoordinateGroupEditView(PermissionRequiredMixin, ObjectEditView):
permission_required = 'netbox_topology_views.change_coordinategroup'
queryset = CoordinateGroup.objects.all()
form = CoordinateGroupsForm
template_name = 'netbox_topology_views/coordinategroup_edit.html'
class CoordinateGroupDeleteView(PermissionRequiredMixin, ObjectDeleteView):
permission_required = 'netbox_topology_views.delete_coordinategroup'
queryset = CoordinateGroup.objects.all()
class CoordinateGroupChangeLogView(PermissionRequiredMixin, ObjectChangeLogView):
permission_required = 'netbox_topology_views.view_coordinategroup'
queryset = CoordinateGroup.objects.all()
class TopologyIndividualOptionsView(PermissionRequiredMixin, View): class TopologyIndividualOptionsView(PermissionRequiredMixin, View):
permission_required = 'netbox_topology_views.change_individualoptions' permission_required = 'netbox_topology_views.change_individualoptions'