Browse Source

Closes #10054: Implement advanced UI controls for object selection (#11952)

* WIP

* WIP

* WIP

* Make object selector functional

* Replace extraneous form fields with selector widgets

* Avoid overlap with filterset field names

* Show checkmarks next to visibile filters

* Update results automatically when searching

* Include selector for device/VM component parent fields

* Use selector for filtering VLAN group/site

* Limit selector to 100 results
Jeremy Stretch 2 years ago
parent
commit
d1f76bec37

+ 6 - 34
netbox/circuits/forms/model_forms.py

@@ -1,7 +1,7 @@
 from django.utils.translation import gettext as _
 
 from circuits.models import *
-from dcim.models import Region, Site, SiteGroup
+from dcim.models import Site
 from ipam.models import ASN
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
@@ -114,50 +114,22 @@ class CircuitTerminationForm(NetBoxModelForm):
             'provider_id': '$provider',
         },
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        },
-        required=False
-    )
-    provider_network_provider = DynamicModelChoiceField(
-        queryset=Provider.objects.all(),
         required=False,
-        label='Provider',
-        initial_params={
-            'networks': 'provider_network'
-        }
+        selector=True
     )
     provider_network = DynamicModelChoiceField(
         queryset=ProviderNetwork.objects.all(),
-        query_params={
-            'provider_id': '$provider_network_provider',
-        },
-        required=False
+        required=False,
+        selector=True
     )
 
     class Meta:
         model = CircuitTermination
         fields = [
-            'provider', 'circuit', 'term_side', 'region', 'site_group', 'site', 'provider_network_provider',
-            'provider_network', 'mark_connected', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
-            'description', 'tags',
+            'provider', 'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed',
+            'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags',
         ]
         widgets = {
             'port_speed': SelectSpeedWidget(),

+ 42 - 296
netbox/dcim/forms/model_forms.py

@@ -14,9 +14,9 @@ from tenancy.forms import TenancyForm
 from utilities.forms import (
     APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField,
     DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SelectWithPK,
-    SlugField, SelectSpeedWidget,
+    SlugField, SelectSpeedWidget
 )
-from virtualization.models import Cluster, ClusterGroup
+from virtualization.models import Cluster
 from wireless.models import WirelessLAN, WirelessLANGroup
 from .common import InterfaceCommonForm, ModuleCommonForm
 
@@ -157,26 +157,9 @@ class SiteForm(TenancyForm, NetBoxModelForm):
 
 
 class LocationForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     parent = DynamicModelChoiceField(
         queryset=Location.objects.all(),
@@ -188,17 +171,14 @@ class LocationForm(TenancyForm, NetBoxModelForm):
     slug = SlugField()
 
     fieldsets = (
-        ('Location', (
-            'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tags',
-        )),
+        ('Location', ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
 
     class Meta:
         model = Location
         fields = (
-            'region', 'site_group', 'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant',
-            'tags',
+            'site', 'parent', 'name', 'slug', 'status', 'description', 'tenant_group', 'tenant', 'tags',
         )
 
 
@@ -219,26 +199,9 @@ class RackRoleForm(NetBoxModelForm):
 
 
 class RackForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
@@ -256,48 +219,16 @@ class RackForm(TenancyForm, NetBoxModelForm):
     class Meta:
         model = Rack
         fields = [
-            'region', 'site_group', 'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status',
-            'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth',
-            'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
+            'site', 'location', 'name', 'facility_id', 'tenant_group', 'tenant', 'status', 'role', 'serial',
+            'asset_tag', 'type', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
+            'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
         ]
 
 
 class RackReservationForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site'
-        }
-    )
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-        }
+        selector=True
     )
     units = NumericArrayField(
         base_field=forms.IntegerField(),
@@ -311,15 +242,14 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Reservation', ('region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')),
+        ('Reservation', ('rack', 'units', 'user', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
 
     class Meta:
         model = RackReservation
         fields = [
-            'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant',
-            'description', 'comments', 'tags',
+            'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
         ]
 
 
@@ -441,26 +371,9 @@ class PlatformForm(NetBoxModelForm):
 
 
 class DeviceForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
@@ -491,43 +404,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
             }
         )
     )
-    manufacturer = DynamicModelChoiceField(
-        queryset=Manufacturer.objects.all(),
-        required=False,
-        initial_params={
-            'device_types': '$device_type'
-        }
-    )
     device_type = DynamicModelChoiceField(
         queryset=DeviceType.objects.all(),
-        query_params={
-            'manufacturer_id': '$manufacturer'
-        }
+        selector=True
     )
     device_role = DynamicModelChoiceField(
         queryset=DeviceRole.objects.all()
     )
     platform = DynamicModelChoiceField(
         queryset=Platform.objects.all(),
-        required=False,
-        query_params={
-            'manufacturer_id': ['$manufacturer', 'null']
-        }
-    )
-    cluster_group = DynamicModelChoiceField(
-        queryset=ClusterGroup.objects.all(),
-        required=False,
-        null_option='None',
-        initial_params={
-            'clusters': '$cluster'
-        }
+        required=False
     )
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
-        query_params={
-            'group_id': '$cluster_group'
-        }
+        selector=True
     )
     comments = CommentField()
     local_context_data = JSONField(
@@ -536,7 +427,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
     )
     virtual_chassis = DynamicModelChoiceField(
         queryset=VirtualChassis.objects.all(),
-        required=False
+        required=False,
+        selector=True
     )
     vc_position = forms.IntegerField(
         required=False,
@@ -556,10 +448,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
     class Meta:
         model = Device
         fields = [
-            'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
-            'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
-            'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority',
-            'description', 'config_template', 'comments', 'tags', 'local_context_data'
+            'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
+            'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster', 'tenant_group', 'tenant',
+            'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'tags',
+            'local_context_data'
         ]
 
     def __init__(self, *args, **kwargs):
@@ -632,18 +524,9 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
             'device_id': '$device'
         }
     )
-    manufacturer = DynamicModelChoiceField(
-        queryset=Manufacturer.objects.all(),
-        required=False,
-        initial_params={
-            'module_types': '$module_type'
-        }
-    )
     module_type = DynamicModelChoiceField(
         queryset=ModuleType.objects.all(),
-        query_params={
-            'manufacturer_id': '$manufacturer'
-        }
+        selector=True
     )
     comments = CommentField()
     replicate_components = forms.BooleanField(
@@ -651,7 +534,6 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
         initial=True,
         help_text=_("Automatically populate components associated with this module type")
     )
-
     adopt_components = forms.BooleanField(
         required=False,
         initial=False,
@@ -659,9 +541,7 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
     )
 
     fieldsets = (
-        ('Module', (
-            'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'description', 'tags',
-        )),
+        ('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
         ('Hardware', (
             'serial', 'asset_tag', 'replicate_components', 'adopt_components',
         )),
@@ -670,8 +550,8 @@ class ModuleForm(ModuleCommonForm, NetBoxModelForm):
     class Meta:
         model = Module
         fields = [
-            'device', 'module_bay', 'manufacturer', 'module_type', 'status', 'serial', 'asset_tag', 'tags',
-            'replicate_components', 'adopt_components', 'description', 'comments',
+            'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', 'tags', 'replicate_components',
+            'adopt_components', 'description', 'comments',
         ]
 
     def __init__(self, *args, **kwargs):
@@ -702,26 +582,9 @@ class CableForm(TenancyForm, NetBoxModelForm):
 
 
 class PowerPanelForm(NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
@@ -733,80 +596,38 @@ class PowerPanelForm(NetBoxModelForm):
     comments = CommentField()
 
     fieldsets = (
-        ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'description', 'tags')),
+        ('Power Panel', ('site', 'location', 'name', 'description', 'tags')),
     )
 
     class Meta:
         model = PowerPanel
         fields = [
-            'region', 'site_group', 'site', 'location', 'name', 'description', 'comments', 'tags',
+            'site', 'location', 'name', 'description', 'comments', 'tags',
         ]
 
 
 class PowerFeedForm(NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites__powerpanel': '$power_panel'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        initial_params={
-            'powerpanel': '$power_panel'
-        },
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
     power_panel = DynamicModelChoiceField(
         queryset=PowerPanel.objects.all(),
-        query_params={
-            'site_id': '$site'
-        }
-    )
-    location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site'
-        },
-        initial_params={
-            'racks': '$rack'
-        }
+        selector=True
     )
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         required=False,
-        query_params={
-            'location_id': '$location',
-            'site_id': '$site'
-        }
+        selector=True
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Power Panel', ('region', 'site', 'power_panel')),
-        ('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
+        ('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
         ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
     )
 
     class Meta:
         model = PowerFeed
         fields = [
-            'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type',
-            'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments',
-            'tags',
+            'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
+            'max_utilization', 'description', 'comments', 'tags',
         ]
 
 
@@ -878,43 +699,12 @@ class DeviceVCMembershipForm(forms.ModelForm):
 
 
 class VCMemberSelectForm(BootstrapMixin, forms.Form):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        required=False,
-        null_option='None',
-        query_params={
-            'site_id': '$site'
-        }
-    )
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         query_params={
-            'site_id': '$site',
-            'rack_id': '$rack',
             'virtual_chassis_id': 'null',
-        }
+        },
+        selector=True
     )
 
     def clean_device(self):
@@ -1150,7 +940,8 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
 
 class DeviceComponentForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
-        queryset=Device.objects.all()
+        queryset=Device.objects.all(),
+        selector=True
     )
 
     def __init__(self, *args, **kwargs):
@@ -1592,53 +1383,9 @@ class InventoryItemRoleForm(NetBoxModelForm):
 
 
 class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    location = DynamicModelChoiceField(
-        queryset=Location.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site'
-        },
-        initial_params={
-            'racks': '$rack'
-        }
-    )
-    rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        required=False,
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-        }
-    )
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
-        query_params={
-            'site_id': '$site',
-            'location_id': '$location',
-            'rack_id': '$rack',
-        }
+        selector=True
     )
     primary_ip4 = DynamicModelChoiceField(
         queryset=IPAddress.objects.all(),
@@ -1660,14 +1407,13 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
     )
 
     fieldsets = (
-        ('Assigned Device', ('region', 'site_group', 'site', 'location', 'rack', 'device')),
-        ('Virtual Device Context', ('name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
+        ('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant'))
     )
 
     class Meta:
         model = VirtualDeviceContext
         fields = [
-            'region', 'site_group', 'site', 'location', 'rack', 'device', 'name', 'status', 'identifier',
-            'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags'
+            'device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant',
+            'comments', 'tags'
         ]

+ 19 - 154
netbox/ipam/forms/model_forms.py

@@ -200,40 +200,11 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
         required=False,
         label=_('VRF')
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        null_option='None',
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    vlan_group = DynamicModelChoiceField(
-        queryset=VLANGroup.objects.all(),
-        required=False,
-        label=_('VLAN group'),
-        null_option='None',
-        query_params={
-            'site': '$site'
-        },
-        initial_params={
-            'vlans': '$vlan'
-        }
+        selector=True,
+        null_option='None'
     )
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
@@ -241,7 +212,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
         label=_('VLAN'),
         query_params={
             'site_id': '$site',
-            'group_id': '$vlan_group',
         }
     )
     role = DynamicModelChoiceField(
@@ -252,7 +222,7 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
 
     fieldsets = (
         ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')),
-        ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')),
+        ('Site/VLAN Assignment', ('site', 'vlan')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
 
@@ -329,65 +299,22 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
         required=False,
         label=_('VRF')
     )
-    nat_region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        label=_('Region'),
-        initial_params={
-            'sites': '$nat_site'
-        }
-    )
-    nat_site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        label=_('Site group'),
-        initial_params={
-            'sites': '$nat_site'
-        }
-    )
-    nat_site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        label=_('Site'),
-        query_params={
-            'region_id': '$nat_region',
-            'group_id': '$nat_site_group',
-        }
-    )
-    nat_rack = DynamicModelChoiceField(
-        queryset=Rack.objects.all(),
-        required=False,
-        label=_('Rack'),
-        null_option='None',
-        query_params={
-            'site_id': '$site'
-        }
-    )
     nat_device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
         required=False,
-        label=_('Device'),
-        query_params={
-            'site_id': '$site',
-            'rack_id': '$nat_rack',
-        }
-    )
-    nat_cluster = DynamicModelChoiceField(
-        queryset=Cluster.objects.all(),
-        required=False,
-        label=_('Cluster')
+        selector=True,
+        label=_('Device')
     )
     nat_virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.all(),
         required=False,
-        label=_('Virtual Machine'),
-        query_params={
-            'cluster_id': '$nat_cluster',
-        }
+        selector=True,
+        label=_('Virtual Machine')
     )
     nat_vrf = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
+        selector=True,
         label=_('VRF')
     )
     nat_inside = DynamicModelChoiceField(
@@ -409,9 +336,8 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
     class Meta:
         model = IPAddress
         fields = [
-            'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_site', 'nat_rack', 'nat_device',
-            'nat_cluster', 'nat_virtual_machine', 'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description',
-            'comments', 'tags',
+            'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'nat_device', 'nat_virtual_machine',
+            'nat_vrf', 'nat_inside', 'tenant_group', 'tenant', 'description', 'comments', 'tags',
         ]
 
     def __init__(self, *args, **kwargs):
@@ -714,58 +640,18 @@ class VLANGroupForm(NetBoxModelForm):
 
 
 class VLANForm(TenancyForm, NetBoxModelForm):
-    # VLANGroup assignment fields
-    scope_type = forms.ChoiceField(
-        choices=(
-            ('', ''),
-            ('dcim.region', 'Region'),
-            ('dcim.sitegroup', 'Site group'),
-            ('dcim.site', 'Site'),
-            ('dcim.location', 'Location'),
-            ('dcim.rack', 'Rack'),
-            ('virtualization.clustergroup', 'Cluster group'),
-            ('virtualization.cluster', 'Cluster'),
-        ),
-        required=False,
-        label=_('Group scope')
-    )
     group = DynamicModelChoiceField(
         queryset=VLANGroup.objects.all(),
         required=False,
-        query_params={
-            'scope_type': '$scope_type',
-        },
+        selector=True,
         label=_('VLAN Group')
     )
-
-    # Site assignment fields
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        },
-        label=_('Region')
-    )
-    sitegroup = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        },
-        label=_('Site group')
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
         null_option='None',
-        query_params={
-            'region_id': '$region',
-            'group_id': '$sitegroup',
-        }
+        selector=True
     )
-
-    # Other fields
     role = DynamicModelChoiceField(
         queryset=Role.objects.all(),
         required=False
@@ -804,11 +690,13 @@ class ServiceTemplateForm(NetBoxModelForm):
 class ServiceForm(NetBoxModelForm):
     device = DynamicModelChoiceField(
         queryset=Device.objects.all(),
-        required=False
+        required=False,
+        selector=True
     )
     virtual_machine = DynamicModelChoiceField(
         queryset=VirtualMachine.objects.all(),
-        required=False
+        required=False,
+        selector=True
     )
     ports = NumericArrayField(
         base_field=forms.IntegerField(
@@ -908,43 +796,21 @@ class L2VPNTerminationForm(NetBoxModelForm):
         label=_('L2VPN'),
         fetch_trigger='open'
     )
-    device_vlan = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        label=_("Available on Device"),
-        required=False,
-        query_params={}
-    )
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        query_params={
-            'available_on_device': '$device_vlan'
-        },
+        selector=True,
         label=_('VLAN')
     )
-    device = DynamicModelChoiceField(
-        queryset=Device.objects.all(),
-        required=False,
-        query_params={}
-    )
     interface = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         required=False,
-        query_params={
-            'device_id': '$device'
-        }
-    )
-    virtual_machine = DynamicModelChoiceField(
-        queryset=VirtualMachine.objects.all(),
-        required=False,
-        query_params={}
+        selector=True
     )
     vminterface = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),
         required=False,
-        query_params={
-            'virtual_machine_id': '$virtual_machine'
-        },
+        selector=True,
         label=_('Interface')
     )
 
@@ -958,7 +824,6 @@ class L2VPNTerminationForm(NetBoxModelForm):
 
         if instance:
             if type(instance.assigned_object) is Interface:
-                initial['device'] = instance.assigned_object.parent
                 initial['interface'] = instance.assigned_object
             elif type(instance.assigned_object) is VLAN:
                 initial['vlan'] = instance.assigned_object

+ 4 - 1
netbox/netbox/urls.py

@@ -10,7 +10,7 @@ from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_a
 from netbox.api.views import APIRootView, StatusView
 from netbox.graphql.schema import schema
 from netbox.graphql.views import GraphQLView
-from netbox.views import HomeView, StaticMediaFailureView, SearchView
+from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
 from users.views import LoginView, LogoutView
 from .admin import admin_site
 
@@ -51,6 +51,9 @@ _patterns = [
     path('virtualization/', include('virtualization.urls')),
     path('wireless/', include('wireless.urls')),
 
+    # HTMX views
+    path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'),
+
     # API
     path('api/', APIRootView.as_view(), name='api-root'),
     path('api/circuits/', include('circuits.api.urls')),

+ 56 - 0
netbox/netbox/views/htmx.py

@@ -0,0 +1,56 @@
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
+from django.http import Http404
+from django.shortcuts import render
+from django.utils.module_loading import import_string
+from django.views.generic import View
+
+
+class ObjectSelectorView(View):
+    template_name = 'htmx/object_selector.html'
+
+    def get(self, request):
+        model = self._get_model(request.GET.get('_model', ''))
+
+        form_class = self._get_form_class(model)
+        form = form_class(request.GET)
+
+        if '_search' in request.GET:
+            # Return only search results
+            filterset = self._get_filterset_class(model)
+
+            queryset = model.objects.restrict(request.user)
+            if filterset:
+                queryset = filterset(request.GET, queryset, request=request).qs
+
+            return render(request, 'htmx/object_selector_results.html', {
+                'results': queryset[:100],
+            })
+
+        return render(request, self.template_name, {
+            'form': form,
+            'model': model,
+            'target_id': request.GET.get('target'),
+        })
+
+    def _get_model(self, label):
+        try:
+            app_label, model_name = label.split('.')
+            content_type = ContentType.objects.get_by_natural_key(app_label, model_name)
+        except (ValueError, ObjectDoesNotExist):
+            raise Http404
+        return content_type.model_class()
+
+    def _get_form_class(self, model):
+        if hasattr(self, 'form_class'):
+            return self.form_class
+        app_label = model._meta.app_label
+        class_name = f'{model.__name__}FilterForm'
+        return import_string(f'{app_label}.forms.{class_name}')
+
+    def _get_filterset_class(self, model):
+        if hasattr(self, 'filterset_class'):
+            return self.filterset_class
+        app_label = model._meta.app_label
+        class_name = f'{model.__name__}FilterSet'
+        return import_string(f'{app_label}.filtersets.{class_name}')

File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js


File diff suppressed because it is too large
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 2 - 1
netbox/project-static/src/htmx.ts

@@ -1,9 +1,10 @@
 import { getElements, isTruthy } from './util';
 import { initButtons } from './buttons';
 import { initSelect } from './select';
+import { initObjectSelector } from './objectSelector';
 
 function initDepedencies(): void {
-  for (const init of [initButtons, initSelect]) {
+  for (const init of [initButtons, initSelect, initObjectSelector]) {
     init();
   }
 }

+ 32 - 0
netbox/project-static/src/objectSelector.ts

@@ -0,0 +1,32 @@
+import { getElements } from './util';
+
+function handleSelection(link: HTMLAnchorElement): void {
+  const selector_results = document.getElementById('selector_results');
+  if (selector_results == null) {
+    return
+  }
+  const target_id = selector_results.getAttribute('data-selector-target');
+  if (target_id == null) {
+    return
+  }
+  const target = document.getElementById(target_id);
+  if (target == null) {
+    return
+  }
+
+  const label = link.getAttribute('data-label');
+  const value = link.getAttribute('data-value');
+
+  //@ts-ignore
+  target.slim.setData([
+    {text: label, value: value}
+  ]);
+
+}
+
+
+export function initObjectSelector(): void {
+  for (const element of getElements<HTMLAnchorElement>('#selector_results a')) {
+    element.addEventListener('click', () => handleSelection(element));
+  }
+}

+ 0 - 3
netbox/templates/circuits/circuittermination_edit.html

@@ -27,12 +27,9 @@
       </div>
       <div class="tab-content p-0 border-0">
         <div class="tab-pane{% if not providernetwork_tab_active %} active{% endif %}" id="site">
-          {% render_field form.region %}
-          {% render_field form.site_group %}
           {% render_field form.site %}
         </div>
         <div class="tab-pane{% if providernetwork_tab_active %} active{% endif %}" id="providernetwork">
-          {% render_field form.provider_network_provider %}
           {% render_field form.provider_network %}
         </div>
       </div>

+ 0 - 4
netbox/templates/dcim/device_edit.html

@@ -18,7 +18,6 @@
       <div class="row mb-2">
         <h5 class="offset-sm-3">Hardware</h5>
       </div>
-      {% render_field form.manufacturer %}
       {% render_field form.device_type %}
       {% render_field form.airflow %}
       {% render_field form.serial %}
@@ -29,8 +28,6 @@
       <div class="row mb-2">
         <h5 class="offset-sm-3">Location</h5>
       </div>
-      {% render_field form.region %}
-      {% render_field form.site_group %}
       {% render_field form.site %}
       {% render_field form.location %}
       {% render_field form.rack %}
@@ -76,7 +73,6 @@
       <div class="row mb-2">
         <h5 class="offset-sm-3">Virtualization</h5>
       </div>
-      {% render_field form.cluster_group %}
       {% render_field form.cluster %}
     </div>
     

+ 0 - 2
netbox/templates/dcim/rack_edit.html

@@ -6,8 +6,6 @@
         <div class="row mb-2">
           <h5 class="offset-sm-3">Rack</h5>
         </div>
-        {% render_field form.region %}
-        {% render_field form.site_group %}
         {% render_field form.site %}
         {% render_field form.location %}
         {% render_field form.name %}

+ 4 - 0
netbox/templates/generic/object_edit.html

@@ -74,3 +74,7 @@ Context:
   </div>
 
 {% endblock content-wrapper %}
+
+{% block modals %}
+  {% include 'inc/htmx_modal.html' with size='lg' %}
+{% endblock %}

+ 32 - 0
netbox/templates/htmx/object_selector.html

@@ -0,0 +1,32 @@
+{% load form_helpers %}
+
+<div class="modal-header">
+  <h5 class="modal-title">Select {{ model|meta:"verbose_name"|bettertitle }}</h5>
+  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+</div>
+<div class="modal-body row">
+  <div class="col-3">
+    <div class="list-group list-group-flush">
+      {% for field in form.visible_fields %}
+        <a href="#" class="list-group-item list-group-item-action px-0 py-1" data-bs-toggle="collapse" data-bs-target="#checkmark{{ forloop.counter }}, #selector{{ forloop.counter }}">
+          <span id="checkmark{{ forloop.counter }}" class="collapse{% if forloop.counter < 3 %} show{% endif %}"><i class="mdi mdi-check-bold"></i></span>
+          {{ field.label }}
+        </a>
+      {% endfor %}
+    </div>
+  </div>
+  <div class="col-9">
+    <form hx-get="{% url 'htmx_object_selector' %}?_model={{ model|meta:"label_lower" }}" hx-target="#selector_results" hx-trigger="load, submit, keyup from:#id_q delay:500ms">
+      <input type="hidden" name="_search" value="true" />
+      <div class="tab-content p-1">
+        {% for field in form.visible_fields %}
+          <div class="collapse{% if forloop.counter < 3 %} show{% endif %}" id="selector{{ forloop.counter }}">{% render_field field %}</div>
+        {% endfor %}
+      </div>
+      <div class="text-end">
+        <button type="submit" class="btn btn-sm btn-primary">Search</button>
+      </div>
+    </form>
+    <div id="selector_results" class="mt-3" data-selector-target="{{ target_id }}"></div>
+  </div>
+</div>

+ 13 - 0
netbox/templates/htmx/object_selector_results.html

@@ -0,0 +1,13 @@
+<div class="list-group">
+  {% for object in results %}
+    <a href="#" class="list-group-item list-group-item-action" data-label="{{ object }}" data-value="{{ object.pk }}" data-bs-dismiss="modal">
+      <h6 class="mb-1">
+        {{ object }}
+        {% if object.status %}{% badge object.get_status_display bg_color=object.get_status_color %}{% endif %}
+      </h6>
+      {% if object.description %}
+        <small>{{ object.description }}</small>
+      {% endif %}
+    </a>
+  {% endfor %}
+</div>

+ 1 - 1
netbox/templates/inc/htmx_modal.html

@@ -1,5 +1,5 @@
 <div class="modal fade" id="htmx-modal" tabindex="-1" aria-hidden="true">
-  <div class="modal-dialog">
+  <div class="modal-dialog{% if size %} modal-{{ size }}{% endif %}">
     <div class="modal-content" id="htmx-modal-content">
       {# Dynamic content goes here #}
     </div>

+ 0 - 5
netbox/templates/ipam/ipaddress_edit.html

@@ -121,14 +121,9 @@
       </div>
       <div class="tab-content p-0 border-0">
           <div class="tab-pane active" id="by_device" aria-labelledby="device_tab" role="tabpanel">
-              {% render_field form.nat_region %}
-              {% render_field form.nat_site_group %}
-              {% render_field form.nat_site %}
-              {% render_field form.nat_rack %}
               {% render_field form.nat_device %}
           </div>
           <div class="tab-pane" id="by_vm" aria-labelledby="vm_tab" role="tabpanel">
-              {% render_field form.nat_cluster %}
               {% render_field form.nat_virtual_machine %}
           </div>
           <div class="tab-pane" id="by_vrf" aria-labelledby="vrf_tab" role="tabpanel">

+ 0 - 3
netbox/templates/ipam/l2vpntermination_edit.html

@@ -32,15 +32,12 @@
     <div class="row mb-3">
       <div class="tab-content p-0 border-0">
         <div class="tab-pane {% if not form.initial.interface or form.initial.vminterface %}active{% endif %}" id="vlan" role="tabpanel" aria-labeled-by="vlan_tab">
-          {% render_field form.device_vlan %}
           {% render_field form.vlan %}
         </div>
         <div class="tab-pane {% if form.initial.interface %}active{% endif %}" id="interface" role="tabpanel" aria-labeled-by="interface_tab">
-          {% render_field form.device %}
           {% render_field form.interface %}
         </div>
         <div class="tab-pane {% if form.initial.vminterface %}active{% endif %}" id="vminterface" role="tabpanel" aria-labeled-by="vminterface_tab">
-          {% render_field form.virtual_machine %}
           {% render_field form.vminterface %}
         </div>
       </div>

+ 0 - 3
netbox/templates/ipam/vlan_edit.html

@@ -43,12 +43,9 @@
       </div>
       <div class="tab-content p-0 border-0">
         <div class="tab-pane{% if not site_tab_active %} active{% endif %}" id="group">
-          {% render_field form.scope_type %}
           {% render_field form.group %}
         </div>
         <div class="tab-pane{% if site_tab_active %} active{% endif %}" id="site">
-          {% render_field form.region %}
-          {% render_field form.sitegroup %}
           {% render_field form.site %}
         </div>
       </div>

+ 21 - 3
netbox/utilities/forms/fields/dynamic.py

@@ -26,24 +26,38 @@ class DynamicModelChoiceMixin:
             choice (optional)
         fetch_trigger: The event type which will cause the select element to
             fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
+        selector: Include an advanced object selection widget to assist the user in identifying the desired object
     """
     filter = django_filters.ModelChoiceFilter
     widget = widgets.APISelect
 
-    def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None,
-                 fetch_trigger=None, empty_label=None, *args, **kwargs):
+    def __init__(
+            self,
+            queryset,
+            *,
+            query_params=None,
+            initial_params=None,
+            null_option=None,
+            disabled_indicator=None,
+            fetch_trigger=None,
+            empty_label=None,
+            selector=False,
+            **kwargs
+    ):
+        self.model = queryset.model
         self.query_params = query_params or {}
         self.initial_params = initial_params or {}
         self.null_option = null_option
         self.disabled_indicator = disabled_indicator
         self.fetch_trigger = fetch_trigger
+        self.selector = selector
 
         # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
         # by widget_attrs()
         self.to_field_name = kwargs.get('to_field_name')
         self.empty_option = empty_label or ""
 
-        super().__init__(*args, **kwargs)
+        super().__init__(queryset, **kwargs)
 
     def widget_attrs(self, widget):
         attrs = {
@@ -70,6 +84,10 @@ class DynamicModelChoiceMixin:
         if (len(self.query_params) > 0):
             widget.add_query_params(self.query_params)
 
+        # Include object selector?
+        if self.selector:
+            attrs['selector'] = self.model._meta.label_lower
+
         return attrs
 
     def get_bound_field(self, form, field_name):

+ 1 - 0
netbox/utilities/forms/widgets.py

@@ -121,6 +121,7 @@ class APISelect(forms.Select):
 
     :param api_url: API endpoint URL. Required if not set automatically by the parent field.
     """
+    template_name = 'widgets/apiselect.html'
     option_template_name = 'widgets/select_option.html'
     dynamic_params: Dict[str, str]
     static_params: Dict[str, List[str]]

+ 18 - 0
netbox/utilities/templates/widgets/apiselect.html

@@ -0,0 +1,18 @@
+{% if widget.attrs.selector %}
+  <div class="d-flex">
+    {% include 'django/forms/widgets/select.html' %}
+    <button
+      type="button"
+      title="Open selector"
+      class="btn btn-sm btn-outline-dark border-input ms-1"
+      data-bs-toggle="modal"
+      data-bs-target="#htmx-modal"
+      hx-get="{% url 'htmx_object_selector' %}?_model={{ widget.attrs.selector }}&target={{ widget.attrs.id }}"
+      hx-target="#htmx-modal-content"
+    >
+      <i class="mdi mdi-database-search-outline"></i>
+    </button>
+  </div>
+{% else %}
+  {% include 'django/forms/widgets/select.html' %}
+{% endif %}

+ 9 - 36
netbox/virtualization/forms/model_forms.py

@@ -65,41 +65,22 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
         queryset=ClusterGroup.objects.all(),
         required=False
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
+        selector=True
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Cluster', ('name', 'type', 'group', 'status', 'description', 'tags')),
-        ('Site', ('region', 'site_group', 'site')),
+        ('Cluster', ('name', 'type', 'group', 'site', 'status', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
     )
 
     class Meta:
         model = Cluster
         fields = (
-            'name', 'type', 'group', 'status', 'tenant', 'region', 'site_group', 'site', 'description', 'comments',
-            'tags',
+            'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments', 'tags',
         )
 
 
@@ -178,20 +159,12 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
         queryset=Site.objects.all(),
         required=False
     )
-    cluster_group = DynamicModelChoiceField(
-        queryset=ClusterGroup.objects.all(),
-        required=False,
-        null_option='None',
-        initial_params={
-            'clusters': '$cluster'
-        }
-    )
     cluster = DynamicModelChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
+        selector=True,
         query_params={
             'site_id': '$site',
-            'group_id': '$cluster_group',
         }
     )
     device = DynamicModelChoiceField(
@@ -222,7 +195,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
     fieldsets = (
         ('Virtual Machine', ('name', 'role', 'status', 'description', 'tags')),
-        ('Site/Cluster', ('site', 'cluster_group', 'cluster', 'device')),
+        ('Site/Cluster', ('site', 'cluster', 'device')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Management', ('platform', 'primary_ip4', 'primary_ip6')),
         ('Resources', ('vcpus', 'memory', 'disk')),
@@ -232,9 +205,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
     class Meta:
         model = VirtualMachine
         fields = [
-            'name', 'status', 'site', 'cluster_group', 'cluster', 'device', 'role', 'tenant_group', 'tenant',
-            'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags',
-            'local_context_data',
+            'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
+            'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags', 'local_context_data',
         ]
 
     def __init__(self, *args, **kwargs):
@@ -280,7 +252,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
 
 class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm):
     virtual_machine = DynamicModelChoiceField(
-        queryset=VirtualMachine.objects.all()
+        queryset=VirtualMachine.objects.all(),
+        selector=True
     )
     parent = DynamicModelChoiceField(
         queryset=VMInterface.objects.all(),

+ 5 - 44
netbox/wireless/forms/model_forms.py

@@ -38,55 +38,16 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
         queryset=WirelessLANGroup.objects.all(),
         required=False
     )
-    region = DynamicModelChoiceField(
-        queryset=Region.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site_group = DynamicModelChoiceField(
-        queryset=SiteGroup.objects.all(),
-        required=False,
-        initial_params={
-            'sites': '$site'
-        }
-    )
-    site = DynamicModelChoiceField(
-        queryset=Site.objects.all(),
-        required=False,
-        null_option='None',
-        query_params={
-            'region_id': '$region',
-            'group_id': '$site_group',
-        }
-    )
-    vlan_group = DynamicModelChoiceField(
-        queryset=VLANGroup.objects.all(),
-        required=False,
-        label=_('VLAN group'),
-        null_option='None',
-        query_params={
-            'site': '$site'
-        },
-        initial_params={
-            'vlans': '$vlan'
-        }
-    )
     vlan = DynamicModelChoiceField(
         queryset=VLAN.objects.all(),
         required=False,
-        label=_('VLAN'),
-        query_params={
-            'site_id': '$site',
-            'group_id': '$vlan_group',
-        }
+        selector=True,
+        label=_('VLAN')
     )
     comments = CommentField()
 
     fieldsets = (
-        ('Wireless LAN', ('ssid', 'group', 'status', 'description', 'tags')),
-        ('VLAN', ('region', 'site_group', 'site', 'vlan_group', 'vlan',)),
+        ('Wireless LAN', ('ssid', 'group', 'vlan', 'status', 'description', 'tags')),
         ('Tenancy', ('tenant_group', 'tenant')),
         ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')),
     )
@@ -94,8 +55,8 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
     class Meta:
         model = WirelessLAN
         fields = [
-            'ssid', 'group', 'region', 'site_group', 'site', 'status', 'vlan_group', 'vlan', 'tenant_group', 'tenant',
-            'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags',
+            'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
+            'description', 'comments', 'tags',
         ]
 
 

Some files were not shown because too many files changed in this diff