Merge pull request #297 from dreng/263_add_multiple_coordinates
Closes #263, Fixes #294: Multiple coordinates
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 260 KiB |
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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=="
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||