diff --git a/netbox_topology_views/api/serializers.py b/netbox_topology_views/api/serializers.py index f6e272f..37c9726 100644 --- a/netbox_topology_views/api/serializers.py +++ b/netbox_topology_views/api/serializers.py @@ -50,4 +50,4 @@ class Meta: class IndividualOptionsSerializer(NetBoxModelSerializer): class Meta: model = IndividualOptions - fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout") + fields = ("ignore_cable_type", "save_coords", "show_unconnected", "show_cables", "show_logical_connections", "show_single_cable_logical_conns", "show_neighbors", "show_circuit", "show_power", "show_wireless", "group_sites", "group_locations", "group_racks", "draw_default_layout", "disable_physics",) diff --git a/netbox_topology_views/api/views.py b/netbox_topology_views/api/views.py index 6741dd8..345a05e 100644 --- a/netbox_topology_views/api/views.py +++ b/netbox_topology_views/api/views.py @@ -109,7 +109,7 @@ def list(self, request): if request.GET: - filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks,show_neighbors = get_query_settings(request) + filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors, disable_physics = get_query_settings(request) # Read options from saved filters as NetBox does not handle custom plugin filters if "filter_id" in request.GET and request.GET["filter_id"] != '': @@ -128,6 +128,7 @@ def list(self, request): if group_locations == False and 'group_locations' in saved_filter_params: group_locations = saved_filter_params['group_locations'] if group_racks == False and 'group_racks' in saved_filter_params: group_racks = saved_filter_params['group_racks'] if show_neighbors == False and 'show_neighbors' in saved_filter_params: show_neighbors = saved_filter_params['show_neighbors'] + if disable_physics == False and 'disable_physics' in saved_filter_params: disable_physics = saved_filter_params['disable_physics'] except SavedFilter.DoesNotExist: # filter_id not found pass except Exception as inst: @@ -153,6 +154,7 @@ def list(self, request): group_locations=group_locations, group_racks=group_racks, group_id=group_id, + disable_physics=disable_physics, ) xml_data = export_data_to_xml(topo_data).decode('utf-8') diff --git a/netbox_topology_views/forms.py b/netbox_topology_views/forms.py index fa3e6ca..2d1ed79 100644 --- a/netbox_topology_views/forms.py +++ b/netbox_topology_views/forms.py @@ -36,7 +36,7 @@ class DeviceFilterForm( FieldSet( 'group', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', 'show_wireless', - 'group_sites', 'group_locations', 'group_racks', name=_("Options") + 'group_sites', 'group_locations', 'group_racks', 'disable_physics', name=_("Options") ), FieldSet('id', name=_("Device")), FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_("Location")), @@ -240,7 +240,7 @@ class DeviceFilterForm( ) show_unconnected = forms.NullBooleanField( label=_('Show Unconnected'), - required=False, + required=False, initial=False, widget=forms.Select( choices=BOOLEAN_WITH_BLANK_CHOICES @@ -310,6 +310,9 @@ class DeviceFilterForm( choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + disable_physics = forms.BooleanField( + label =_('Disable Physics'), required=False, initial=False + ) class CoordinateGroupsForm(NetBoxModelForm): fieldsets = ( @@ -502,6 +505,7 @@ class IndividualOptionsForm(NetBoxModelForm): 'group_locations', 'group_racks', 'draw_default_layout', + 'disable_physics', ), ) @@ -629,6 +633,13 @@ class IndividualOptionsForm(NetBoxModelForm): help_text=_('Enable this option if you want to draw the topology on ' 'the initial load (when you go to the topology plugin page).') ) + disable_physics = forms.BooleanField( + label=('Disable Physics'), + required=False, + initial=False, + help_text=_('When enables, no forces will act on nodes in the topology and they will only move ' + 'when dragged by hand. Devices without coordinates will be placed at (0, 0) by default.') + ) class Meta: model = IndividualOptions @@ -636,5 +647,6 @@ class Meta: 'user_id', 'ignore_cable_type', 'preselected_device_roles', 'preselected_tags', 'save_coords', 'show_unconnected', 'show_cables', 'show_logical_connections', 'show_single_cable_logical_conns', 'show_neighbors', 'show_circuit', 'show_power', - 'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'draw_default_layout' + 'show_wireless', 'group_sites', 'group_locations', 'group_racks', 'draw_default_layout', + 'disable_physics' ] diff --git a/netbox_topology_views/migrations/0009_disable_physics.py b/netbox_topology_views/migrations/0009_disable_physics.py new file mode 100644 index 0000000..c3c7d4d --- /dev/null +++ b/netbox_topology_views/migrations/0009_disable_physics.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-04-29 14:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_topology_views', '0007_individualoptions_group_locations_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='individualoptions', + name='disable_physics', + field=models.BooleanField(default=False), + ), + ] \ No newline at end of file diff --git a/netbox_topology_views/models.py b/netbox_topology_views/models.py index 7c14a47..9d7ce01 100644 --- a/netbox_topology_views/models.py +++ b/netbox_topology_views/models.py @@ -398,6 +398,9 @@ class IndividualOptions(NetBoxModel): draw_default_layout = models.BooleanField( default=False ) + disable_physics = models.BooleanField( + default=False + ) _netbox_private = True diff --git a/netbox_topology_views/utils.py b/netbox_topology_views/utils.py index 2217012..49450d8 100644 --- a/netbox_topology_views/utils.py +++ b/netbox_topology_views/utils.py @@ -177,8 +177,13 @@ def get_query_settings(request): if "show_neighbors" in request.GET: if request.GET["show_neighbors"] == "True" : show_neighbors = True - - return filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors + + disable_physics = False + if "disable_physics" in request.GET: + if request.GET["disable_physics"] == "on": + disable_physics = True + + return filter_id, save_coords, show_unconnected, show_power, show_circuit, show_logical_connections, show_single_cable_logical_conns, show_cables, show_wireless, group_sites, group_locations, group_racks, show_neighbors, disable_physics class LinePattern(): wireless = [2, 10, 2, 10] diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index a2b4978..b0c32a6 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -1,5 +1,6 @@ import json from functools import reduce +import random from typing import DefaultDict, Dict, Optional, Union import time from itertools import chain @@ -97,7 +98,7 @@ def get_image_for_entity(entity: Union[Device, Circuit, PowerPanel, PowerFeed]): def create_node( - device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, group_id="default" + device: Union[Device, Circuit, PowerPanel, PowerFeed], save_coords: bool, disable_physics: bool, group_id="default", nnodes=1, ): node = {} node_content = "" @@ -200,11 +201,12 @@ def create_node( 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 + node["physics"] = not disable_physics + + # 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 @@ -222,6 +224,21 @@ def create_node( node["x"] = int(cords[0]) node["y"] = int(cords[1]) node["physics"] = False + elif disable_physics: + # draw devices clustered by their rack neighbours + # for center coordinates of a rack, use the rack name as seed (if available) + random.seed(device.rack.name if hasattr(device, "rack") else device.name) + # set the upper size of the graph + plot_size = int(nnodes / 12) + 1 + base_coords_x = random.randint(-plot_size * nnodes, plot_size * nnodes) + base_coords_y = random.randint(-plot_size * nnodes, plot_size * nnodes) + # make seed unique for devices for spread + if hasattr(device, "rack"): + random.seed(device.name) + # spread devices around the rack center coordinates + node["x"] = base_coords_x + random.randint(-2 * nnodes, 2 * nnodes) + node["y"] = base_coords_y + random.randint(-2 * nnodes, 2 * nnodes) + dev_title = "