Просмотр исходного кода

Merge branch 'feature-object-filter' into feature

thatmattlove 4 лет назад
Родитель
Сommit
8bdfa34c7d

+ 16 - 8
netbox/circuits/forms.py

@@ -113,7 +113,8 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -121,7 +122,8 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     asn = forms.IntegerField(
         required=False,
@@ -198,7 +200,8 @@ class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         required=False,
-        label=_('Provider')
+        label=_('Provider'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -368,12 +371,14 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
     type_id = DynamicModelMultipleChoiceField(
         queryset=CircuitType.objects.all(),
         required=False,
-        label=_('Type')
+        label=_('Type'),
+        fetch_trigger='open'
     )
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         required=False,
-        label=_('Provider')
+        label=_('Provider'),
+        fetch_trigger='open'
     )
     provider_network_id = DynamicModelMultipleChoiceField(
         queryset=ProviderNetwork.objects.all(),
@@ -381,7 +386,8 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
         query_params={
             'provider_id': '$provider_id'
         },
-        label=_('Provider network')
+        label=_('Provider network'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=CircuitStatusChoices,
@@ -391,7 +397,8 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -399,7 +406,8 @@ class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     commit_rate = forms.IntegerField(
         required=False,

+ 123 - 62
netbox/dcim/forms.py

@@ -71,12 +71,14 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -84,7 +86,8 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
@@ -92,7 +95,8 @@ class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Device')
+        label=_('Device'),
+        fetch_trigger='open'
     )
 
 
@@ -457,17 +461,19 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
     status = forms.MultipleChoiceField(
         choices=SiteStatusChoices,
         required=False,
-        widget=StaticSelectMultiple()
+        widget=StaticSelectMultiple(),
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Group')
+        label=_('Group'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -565,7 +571,8 @@ class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -573,7 +580,8 @@ class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     parent_id = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(),
@@ -582,7 +590,8 @@ class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
             'region_id': '$region_id',
             'site_id': '$site_id',
         },
-        label=_('Parent')
+        label=_('Parent'),
+        fetch_trigger='open'
     )
 
 
@@ -862,7 +871,8 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -870,7 +880,8 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     location_id = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(),
@@ -879,7 +890,8 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Location')
+        label=_('Location'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=RackStatusChoices,
@@ -900,7 +912,8 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
         queryset=RackRole.objects.all(),
         required=False,
         null_option='None',
-        label=_('Role')
+        label=_('Role'),
+        fetch_trigger='open'
     )
     asset_tag = forms.CharField(
         required=False
@@ -923,7 +936,8 @@ class RackElevationFilterForm(RackFilterForm):
         query_params={
             'site_id': '$site_id',
             'location_id': '$location_id',
-        }
+        },
+        fetch_trigger='open'
     )
 
 
@@ -937,14 +951,16 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         required=False,
         initial_params={
             'sites': '$site'
-        }
+        },
+        fetch_trigger='open'
     )
     site_group = DynamicModelChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
         initial_params={
             'sites': '$site'
-        }
+        },
+        fetch_trigger='open'
     )
     site = DynamicModelChoiceField(
         queryset=Site.objects.all(),
@@ -952,21 +968,24 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
         query_params={
             'region_id': '$region',
             'group_id': '$site_group',
-        }
+        },
+        fetch_trigger='open'
     )
     location = DynamicModelChoiceField(
         queryset=Location.objects.all(),
         required=False,
         query_params={
             'site_id': '$site'
-        }
+        },
+        fetch_trigger='open'
     )
     rack = DynamicModelChoiceField(
         queryset=Rack.objects.all(),
         query_params={
             'site_id': '$site',
             'location_id': '$location',
-        }
+        },
+        fetch_trigger='open'
     )
     units = NumericArrayField(
         base_field=forms.IntegerField(),
@@ -980,7 +999,8 @@ class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     tags = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
-        required=False
+        required=False,
+        fetch_trigger='open'
     )
 
     class Meta:
@@ -1080,7 +1100,8 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -1088,13 +1109,15 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     location_id = DynamicModelMultipleChoiceField(
         queryset=Location.objects.prefetch_related('site'),
         required=False,
         label=_('Location'),
-        null_option='None'
+        null_option='None',
+        fetch_trigger='open'
     )
     user_id = DynamicModelMultipleChoiceField(
         queryset=User.objects.all(),
@@ -1102,7 +1125,8 @@ class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMo
         label=_('User'),
         widget=APISelectMultiple(
             api_url='/api/users/users/',
-        )
+        ),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -1231,7 +1255,8 @@ class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
-        label=_('Manufacturer')
+        label=_('Manufacturer'),
+        fetch_trigger='open'
     )
     subdevice_role = forms.MultipleChoiceField(
         choices=add_blank_choice(SubdeviceRoleChoices),
@@ -2036,7 +2061,8 @@ class PlatformFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
-        label=_('Manufacturer')
+        label=_('Manufacturer'),
+        fetch_trigger='open'
     )
 
 
@@ -2452,7 +2478,8 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -2460,7 +2487,8 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     location_id = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(),
@@ -2469,7 +2497,8 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Location')
+        label=_('Location'),
+        fetch_trigger='open'
     )
     rack_id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
@@ -2479,17 +2508,20 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
             'site_id': '$site_id',
             'location_id': '$location_id',
         },
-        label=_('Rack')
+        label=_('Rack'),
+        fetch_trigger='open'
     )
     role_id = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
         required=False,
-        label=_('Role')
+        label=_('Role'),
+        fetch_trigger='open'
     )
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
-        label=_('Manufacturer')
+        label=_('Manufacturer'),
+        fetch_trigger='open'
     )
     device_type_id = DynamicModelMultipleChoiceField(
         queryset=DeviceType.objects.all(),
@@ -2497,13 +2529,15 @@ class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilt
         query_params={
             'manufacturer_id': '$manufacturer_id'
         },
-        label=_('Model')
+        label=_('Model'),
+        fetch_trigger='open'
     )
     platform_id = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         required=False,
         null_option='None',
-        label=_('Platform')
+        label=_('Platform'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=DeviceStatusChoices,
@@ -3987,7 +4021,8 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
-        label=_('Manufacturer')
+        label=_('Manufacturer'),
+        fetch_trigger='open'
     )
     serial = forms.CharField(
         required=False
@@ -4461,7 +4496,8 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -4469,12 +4505,14 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     tenant_id = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
-        label=_('Tenant')
+        label=_('Tenant'),
+        fetch_trigger='open'
     )
     rack_id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
@@ -4483,7 +4521,8 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         null_option='None',
         query_params={
             'site_id': '$site_id'
-        }
+        },
+        fetch_trigger='open'
     )
     type = forms.MultipleChoiceField(
         choices=add_blank_choice(CableTypeChoices),
@@ -4506,7 +4545,8 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
             'tenant_id': '$tenant_id',
             'rack_id': '$rack_id',
         },
-        label=_('Device')
+        label=_('Device'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -4519,7 +4559,8 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -4527,7 +4568,8 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
@@ -4535,7 +4577,8 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Device')
+        label=_('Device'),
+        fetch_trigger='open'
     )
 
 
@@ -4543,7 +4586,8 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -4551,7 +4595,8 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
@@ -4559,7 +4604,8 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Device')
+        label=_('Device'),
+        fetch_trigger='open'
     )
 
 
@@ -4567,7 +4613,8 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -4575,7 +4622,8 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
@@ -4583,7 +4631,8 @@ class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Device')
+        label=_('Device'),
+        fetch_trigger='open'
     )
 
 
@@ -4837,12 +4886,14 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -4850,7 +4901,8 @@ class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -4973,12 +5025,14 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -4986,7 +5040,8 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     location_id = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(),
@@ -4995,7 +5050,8 @@ class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Location')
+        label=_('Location'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -5213,12 +5269,14 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -5226,7 +5284,8 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     power_panel_id = DynamicModelMultipleChoiceField(
         queryset=PowerPanel.objects.all(),
@@ -5235,7 +5294,8 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Power panel')
+        label=_('Power panel'),
+        fetch_trigger='open'
     )
     rack_id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
@@ -5244,7 +5304,8 @@ class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         query_params={
             'site_id': '$site_id'
         },
-        label=_('Rack')
+        label=_('Rack'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=PowerFeedStatusChoices,

+ 30 - 15
netbox/extras/forms.py

@@ -676,58 +676,69 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Regions')
+        label=_('Regions'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site groups')
+        label=_('Site groups'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        label=_('Sites')
+        label=_('Sites'),
+        fetch_trigger='open'
     )
     device_type_id = DynamicModelMultipleChoiceField(
         queryset=DeviceType.objects.all(),
         required=False,
-        label=_('Device types')
+        label=_('Device types'),
+        fetch_trigger='open'
     )
     role_id = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
         required=False,
-        label=_('Roles')
+        label=_('Roles'),
+        fetch_trigger='open'
     )
     platform_id = DynamicModelMultipleChoiceField(
         queryset=Platform.objects.all(),
         required=False,
-        label=_('Platforms')
+        label=_('Platforms'),
+        fetch_trigger='open'
     )
     cluster_group_id = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         required=False,
-        label=_('Cluster groups')
+        label=_('Cluster groups'),
+        fetch_trigger='open'
     )
     cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
-        label=_('Clusters')
+        label=_('Clusters'),
+        fetch_trigger='open'
     )
     tenant_group_id = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         required=False,
-        label=_('Tenant groups')
+        label=_('Tenant groups'),
+        fetch_trigger='open'
     )
     tenant_id = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
         required=False,
-        label=_('Tenant')
+        label=_('Tenant'),
+        fetch_trigger='open'
     )
     tag = DynamicModelMultipleChoiceField(
         queryset=Tag.objects.all(),
         to_field_name='slug',
         required=False,
-        label=_('Tags')
+        label=_('Tags'),
+        fetch_trigger='open'
     )
 
 
@@ -820,7 +831,8 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
         label=_('User'),
         widget=APISelectMultiple(
             api_url='/api/users/users/',
-        )
+        ),
+        fetch_trigger='open'
     )
     assigned_object_type_id = DynamicModelMultipleChoiceField(
         queryset=ContentType.objects.all(),
@@ -828,7 +840,8 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
         label=_('Object Type'),
         widget=APISelectMultiple(
             api_url='/api/extras/content-types/',
-        )
+        ),
+        fetch_trigger='open'
     )
     kind = forms.ChoiceField(
         choices=add_blank_choice(JournalEntryKindChoices),
@@ -868,7 +881,8 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
         label=_('User'),
         widget=APISelectMultiple(
             api_url='/api/users/users/',
-        )
+        ),
+        fetch_trigger='open'
     )
     changed_object_type_id = DynamicModelMultipleChoiceField(
         queryset=ContentType.objects.all(),
@@ -876,7 +890,8 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
         label=_('Object Type'),
         widget=APISelectMultiple(
             api_url='/api/extras/content-types/',
-        )
+        ),
+        fetch_trigger='open'
     )
 
 

+ 50 - 25
netbox/ipam/forms.py

@@ -115,12 +115,14 @@ class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFor
     import_target_id = DynamicModelMultipleChoiceField(
         queryset=RouteTarget.objects.all(),
         required=False,
-        label=_('Import targets')
+        label=_('Import targets'),
+        fetch_trigger='open'
     )
     export_target_id = DynamicModelMultipleChoiceField(
         queryset=RouteTarget.objects.all(),
         required=False,
-        label=_('Export targets')
+        label=_('Export targets'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -185,12 +187,14 @@ class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelF
     importing_vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label=_('Imported by VRF')
+        label=_('Imported by VRF'),
+        fetch_trigger='open'
     )
     exporting_vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label=_('Exported by VRF')
+        label=_('Exported by VRF'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -345,7 +349,8 @@ class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
     rir_id = DynamicModelMultipleChoiceField(
         queryset=RIR.objects.all(),
         required=False,
-        label=_('RIR')
+        label=_('RIR'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -642,12 +647,14 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
         queryset=VRF.objects.all(),
         required=False,
         label=_('Assigned VRF'),
-        null_option='Global'
+        null_option='Global',
+        fetch_trigger='open'
     )
     present_in_vrf_id = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label=_('Present in VRF')
+        label=_('Present in VRF'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=PrefixStatusChoices,
@@ -657,12 +664,14 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -671,13 +680,15 @@ class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilter
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     role_id = DynamicModelMultipleChoiceField(
         queryset=Role.objects.all(),
         required=False,
         null_option='None',
-        label=_('Role')
+        label=_('Role'),
+        fetch_trigger='open'
     )
     is_pool = forms.NullBooleanField(
         required=False,
@@ -818,7 +829,8 @@ class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
         queryset=VRF.objects.all(),
         required=False,
         label=_('Assigned VRF'),
-        null_option='Global'
+        null_option='Global',
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=PrefixStatusChoices,
@@ -829,7 +841,8 @@ class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
         queryset=Role.objects.all(),
         required=False,
         null_option='None',
-        label=_('Role')
+        label=_('Role'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -1265,12 +1278,14 @@ class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
         queryset=VRF.objects.all(),
         required=False,
         label=_('Assigned VRF'),
-        null_option='Global'
+        null_option='Global',
+        fetch_trigger='open'
     )
     present_in_vrf_id = DynamicModelChoiceField(
         queryset=VRF.objects.all(),
         required=False,
-        label=_('Present in VRF')
+        label=_('Present in VRF'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=IPAddressStatusChoices,
@@ -1439,27 +1454,32 @@ class VLANGroupFilterForm(BootstrapMixin, forms.Form):
     region = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     sitegroup = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
         required=False,
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     location = DynamicModelMultipleChoiceField(
         queryset=Location.objects.all(),
         required=False,
-        label=_('Location')
+        label=_('Location'),
+        fetch_trigger='open'
     )
     rack = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
         required=False,
-        label=_('Rack')
+        label=_('Rack'),
+        fetch_trigger='open'
     )
 
 
@@ -1652,12 +1672,14 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -1666,7 +1688,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
         query_params={
             'region': '$region'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     group_id = DynamicModelMultipleChoiceField(
         queryset=VLANGroup.objects.all(),
@@ -1675,7 +1698,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
         query_params={
             'region': '$region'
         },
-        label=_('VLAN group')
+        label=_('VLAN group'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=VLANStatusChoices,
@@ -1686,7 +1710,8 @@ class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
         queryset=Role.objects.all(),
         required=False,
         null_option='None',
-        label=_('Role')
+        label=_('Role'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 84 - 17
netbox/project-static/src/select/api.ts

@@ -2,7 +2,7 @@ import queryString from 'query-string';
 import { readableColor } from 'color2k';
 import SlimSelect from 'slim-select';
 import { createToast } from '../bs';
-import { hasUrl, hasExclusions } from './util';
+import { hasUrl, hasExclusions, isTrigger } from './util';
 import {
   isTruthy,
   hasError,
@@ -10,6 +10,7 @@ import {
   getApiData,
   isApiError,
   getElements,
+  createElement,
   findFirstAdjacent,
 } from '../util';
 
@@ -17,6 +18,20 @@ import type { Option } from 'slim-select/dist/data';
 
 type QueryFilter = Map<string, string | number | boolean>;
 
+export type Trigger =
+  /**
+   * Load data when the select element is opened.
+   */
+  | 'open'
+  /**
+   * Load data when the element is loaded.
+   */
+  | 'load'
+  /**
+   * Load data when a parent element is uncollapsed.
+   */
+  | 'collapse';
+
 // Various one-off patterns to replace in query param keys.
 const REPLACE_PATTERNS = [
   // Don't query `termination_a_device=1`, but rather `device=1`.
@@ -57,6 +72,17 @@ class APISelect {
    */
   public readonly placeholder: string;
 
+  /**
+   * Event that will initiate the API call to NetBox to load option data. By default, the trigger
+   * is `'load'`, so data will be fetched when the element renders on the page.
+   */
+  private readonly trigger: Trigger;
+
+  /**
+   * If `true`, a refresh button will be added next to the search/filter `<input/>` element.
+   */
+  private readonly allowRefresh: boolean = true;
+
   /**
    * Event to be dispatched when dependent fields' values change.
    */
@@ -153,6 +179,7 @@ class APISelect {
       allowDeselect: true,
       deselectLabel: `<i class="mdi mdi-close-circle" style="color:currentColor;"></i>`,
       placeholder: this.placeholder,
+      searchPlaceholder: 'Filter',
       onChange: () => this.handleSlimChange(),
     });
 
@@ -186,20 +213,44 @@ class APISelect {
     // Initialize controlling elements.
     this.initResetButton();
 
+    // Add the refresh button to the search element.
+    this.initRefreshButton();
+
     // Add dependency event listeners.
     this.addEventListeners();
 
+    // Determine if the fetch trigger has been set.
+    const triggerAttr = this.base.getAttribute('data-fetch-trigger');
+
     // Determine if this element is part of collapsible element.
     const collapse = this.base.closest('.content-container .collapse');
-    if (collapse !== null) {
-      // If this element is part of a collapsible element, only load the data when the
-      // collapsible element is shown.
-      // See: https://getbootstrap.com/docs/5.0/components/collapse/#events
-      collapse.addEventListener('show.bs.collapse', () => this.loadData());
-      collapse.addEventListener('hide.bs.collapse', () => this.resetOptions());
+
+    if (isTrigger(triggerAttr)) {
+      this.trigger = triggerAttr;
+    } else if (collapse !== null) {
+      this.trigger = 'collapse';
     } else {
-      // Otherwise, load the data on render.
-      Promise.all([this.loadData()]);
+      this.trigger = 'load';
+    }
+
+    switch (this.trigger) {
+      case 'collapse':
+        if (collapse !== null) {
+          // If this element is part of a collapsible element, only load the data when the
+          // collapsible element is shown.
+          // See: https://getbootstrap.com/docs/5.0/components/collapse/#events
+          collapse.addEventListener('show.bs.collapse', () => this.loadData());
+          collapse.addEventListener('hide.bs.collapse', () => this.resetOptions());
+        }
+        break;
+      case 'open':
+        // If the trigger is 'open', only load API data when the select element is opened.
+        this.slim.beforeOpen = () => this.loadData();
+        break;
+      case 'load':
+        // Otherwise, load the data immediately.
+        Promise.all([this.loadData()]);
+        break;
     }
   }
 
@@ -713,21 +764,37 @@ class APISelect {
   }
 
   /**
-   * Initialize any adjacent reset buttons so that when clicked, the instance's selected value is cleared.
+   * Initialize any adjacent reset buttons so that when clicked, the page is reloaded without
+   * query parameters.
    */
   private initResetButton(): void {
-    const resetButton = findFirstAdjacent<HTMLButtonElement>(this.base, 'button[data-reset-select');
+    const resetButton = findFirstAdjacent<HTMLButtonElement>(
+      this.base,
+      'button[data-reset-select]',
+    );
     if (resetButton !== null) {
       resetButton.addEventListener('click', () => {
-        this.base.value = '';
-        if (this.base.multiple) {
-          this.slim.setSelected([]);
-        } else {
-          this.slim.setSelected('');
-        }
+        window.location.assign(window.location.origin + window.location.pathname);
       });
     }
   }
+
+  /**
+   * Add a refresh button to the search container element. When clicked, the API data will be
+   * reloaded.
+   */
+  private initRefreshButton(): void {
+    if (this.allowRefresh) {
+      const refreshButton = createElement(
+        'button',
+        { type: 'button' },
+        ['btn', 'btn-sm', 'btn-ghost-dark'],
+        [createElement('i', {}, ['mdi', 'mdi-reload'])],
+      );
+      refreshButton.addEventListener('click', () => this.loadData());
+      this.slim.slim.search.container.appendChild(refreshButton);
+    }
+  }
 }
 
 export function initApiSelect() {

+ 9 - 0
netbox/project-static/src/select/util.ts

@@ -1,3 +1,5 @@
+import type { Trigger } from './api';
+
 /**
  * Determine if an element has the `data-url` attribute set.
  */
@@ -15,3 +17,10 @@ export function hasExclusions(
   const exclude = el.getAttribute('data-query-param-exclude');
   return typeof exclude === 'string' && exclude !== '';
 }
+
+/**
+ * Determine if a trigger value is valid.
+ */
+export function isTrigger(value: unknown): value is Trigger {
+  return typeof value === 'string' && ['load', 'open', 'collapse'].includes(value);
+}

+ 1 - 0
netbox/project-static/styles/netbox.scss

@@ -268,6 +268,7 @@ div.title-container {
 nav.search {
   // Don't overtake dropdowns
   z-index: 999;
+  justify-content: center;
   background-color: var(--nbx-body-bg);
 
   form button.dropdown-toggle {

+ 30 - 1
netbox/project-static/styles/select.scss

@@ -72,8 +72,8 @@ $spacing-s: $input-padding-x;
         border-color: currentColor;
       }
     }
+    // Don't show the depth indicator outside of the menu.
     .placeholder .depth {
-      // Don't show the depth indicator outside of the menu.
       display: none;
     }
     span.placeholder > *,
@@ -94,6 +94,11 @@ $spacing-s: $input-padding-x;
       .ss-value {
         color: var(--nbx-select-value-color);
         border-radius: $badge-border-radius;
+
+        // Don't show the depth indicator outside of the menu.
+        .depth {
+          display: none;
+        }
       }
     }
     .ss-add {
@@ -133,10 +138,34 @@ $spacing-s: $input-padding-x;
           opacity: 0.3;
         }
       }
+
+      &::-webkit-scrollbar {
+        right: 0;
+        width: 4px;
+        &:hover {
+          opacity: 0.8;
+        }
+      }
+
+      &::-webkit-scrollbar-track {
+        background: transparent;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        right: 0;
+        width: 2px;
+        background-color: var(--nbx-sidebar-scroll);
+      }
     }
     border-bottom-right-radius: $form-select-border-radius;
     border-bottom-left-radius: $form-select-border-radius;
     .ss-search {
+      padding-right: $spacer * 0.5;
+
+      button {
+        margin-left: $spacer * 0.75;
+      }
+
       input[type='search'] {
         color: $input-color;
         background-color: $form-select-bg;

+ 12 - 30
netbox/templates/dcim/connections_list.html

@@ -1,42 +1,24 @@
 {% extends 'base/layout.html' %}
 {% load buttons %}
+{% load render_table from django_tables2 %}
 
 {% block title %}{{ title }}{% endblock %}
 
 {% block extra_controls %}{% export_button content_type %}{% endblock %}
 
 {% block content %}
-{% if filter_form %}
-    <div class="col col-md-12 noprint">
-        {% include 'inc/advanced_search.html' %}
-    </div>
-{% endif %}
-<div class="row mb-3">
-    <div class="col col-md-12">
-        <div class="card">
-            <div class="card-header">
-                <div class="row">
-                    <div class="col col-md-4 offset-md-8 d-flex noprint table-controls">
-                        <div class="input-group input-group-sm">
-                            <input type="text" class="form-control object-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
-                            {% if filter_form %}
-                                <button
-                                    type="button"
-                                    class="btn btn-sm btn-outline-dark"
-                                    data-bs-toggle="collapse"
-                                    data-bs-target="#advanced-search-content">
-                                    Advanced Search
-                                </button>
-                            {% endif %}
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="card-body">
-                {% include 'inc/responsive_table.html' %}
+    <div class="row mb-3">
+        <div class="col col-md-7 col-lg-8 col-xl-9 col-xxl-10">
+            {% include 'inc/table_controls.html' %}
+
+            <div class="table-responsive">
+                {% render_table table 'inc/table.html' %}
             </div>
+
+            {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
         </div>
-        {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+        {% if filter_form %}
+            {% include 'inc/filter_list.html' %}
+        {% endif %}
     </div>
-</div>
 {% endblock %}

+ 46 - 51
netbox/templates/dcim/rack_elevation_list.html

@@ -5,63 +5,58 @@
 {% block title %}Rack Elevations{% endblock %}
 
 {% block controls %}
-<div class="container mb-2 mx-0">
-    <div class="d-flex flex-wrap justify-content-end">
-        <button type="button" class="btn btn-sm btn-outline-dark m-1" data-bs-toggle="collapse" data-bs-target="#advanced-search-content">
-            Advanced Search
-        </button>
-        <button class="btn btn-sm btn-outline-dark toggle-images m-1" selected="selected">
-            <span class="mdi mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
-        </button>
-        <div class="btn-group btn-group-sm m-1" role="group">
-            <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
-            <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
-        </div>
-        <div class="btn-group btn-group-sm m-1" role="group">
-            <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
-            <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
+    <div class="container mb-2 mx-0">
+        <div class="d-flex flex-wrap justify-content-end">
+            <button class="btn btn-sm btn-outline-dark toggle-images m-1" selected="selected">
+                <span class="mdi mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
+            </button>
+            <div class="btn-group btn-group-sm m-1" role="group">
+                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
+                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
+            </div>
+            <div class="btn-group btn-group-sm m-1" role="group">
+                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
+                <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
+            </div>
         </div>
     </div>
-</div>
 {% endblock %}
 
 {% block content %}
-<div class="col col-md-12 noprint">
-    {% include 'inc/advanced_search.html' %}
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% if page %}
-            <div style="white-space: nowrap; overflow-x: scroll;">
-                {% for rack in page %}
-                    <div style="display: inline-block; margin-right: 12px; width: 254px">
-                      <div style="margin-left: 30px">
-                        <div class="text-center">
-                            <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
-                            {% if rack.role %}
-                                <br /><span class="badge my-3" style="color: {{ rack.role.color|fgcolor }}; background-color: #{{ rack.role.color }}">{{ rack.role }}</span>
-                            {% endif %}
-                            {% if rack.facility_id %}
-                                <br /><small class="text-muted">{{ rack.facility_id }}</small>
-                            {% endif %}
+    <div class="row">
+        <div class="col col-md-7 col-lg-8 col-xl-9 col-xxl-10">
+            {% if page %}
+                <div style="white-space: nowrap; overflow-x: scroll;">
+                    {% for rack in page %}
+                        <div style="display: inline-block; margin-right: 12px; width: 254px">
+                        <div style="margin-left: 30px">
+                            <div class="text-center">
+                                <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
+                                {% if rack.role %}
+                                    <br /><span class="badge my-3" style="color: {{ rack.role.color|fgcolor }}; background-color: #{{ rack.role.color }}">{{ rack.role }}</span>
+                                {% endif %}
+                                {% if rack.facility_id %}
+                                    <br /><small class="text-muted">{{ rack.facility_id }}</small>
+                                {% endif %}
+                            </div>
+                            {% include 'dcim/inc/rack_elevation.html' with object=rack face=rack_face %}
+                            <div class="clearfix"></div>
+                            <div class="text-center">
+                                <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
+                                {% if rack.facility_id %}
+                                    <small class="text-muted">({{ rack.facility_id }})</small>
+                                {% endif %}
+                            </div>
                         </div>
-                        {% include 'dcim/inc/rack_elevation.html' with object=rack face=rack_face %}
-                        <div class="clearfix"></div>
-                        <div class="text-center">
-                            <strong><a href="{% url 'dcim:rack' pk=rack.pk %}">{{ rack.name }}</a></strong>
-                            {% if rack.facility_id %}
-                                <small class="text-muted">({{ rack.facility_id }})</small>
-                            {% endif %}
                         </div>
-                      </div>
-                    </div>
-                {% endfor %}
-            </div>
-            <br />
-            {% include 'inc/paginator.html' %}
-        {% else %}
-            <p>No Racks Found</p>
-        {% endif %}
+                    {% endfor %}
+                </div>
+                <br />
+                {% include 'inc/paginator.html' %}
+            {% else %}
+                <p>No Racks Found</p>
+            {% endif %}
+        </div>
+        {% include 'inc/filter_list.html' %}
     </div>
-</div>
 {% endblock %}

+ 7 - 7
netbox/templates/generic/object_list.html

@@ -24,9 +24,6 @@
 {% endblock controls %}
 
 {% block content %}
-{% if filter_form %}
-  {% include 'inc/advanced_search.html' %}
-{% endif %}
 {% if table.paginator.num_pages > 1 %}
 {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
   <div id="select-all-box" class="d-none card noprint">
@@ -57,12 +54,12 @@
 {% endwith %}
 {% endif %}
 
-{# Object list filter, table config #}
-{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
-
 {# Object table #}
 <div class="row">
-  <div class="col col-md-12">
+  <div class="col col-md-7 col-lg-8 col-xl-9 col-xxl-10">
+    {# Object list filter, table config #}
+    {% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
+
     {% with bulk_edit_url=content_type.model_class|validated_viewname:"bulk_edit" bulk_delete_url=content_type.model_class|validated_viewname:"bulk_delete" %}
       {% if permissions.change or permissions.delete %}
         <form method="post" class="form form-horizontal">
@@ -95,6 +92,9 @@
     {% endwith %}
     {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
   </div>
+  {% if filter_form %}
+    {% include 'inc/filter_list.html' %}
+  {% endif %}
 </div>
 {% table_config_form table table_name="ObjectTable" %}
 {% endblock content %}

+ 62 - 0
netbox/templates/inc/filter_list.html

@@ -0,0 +1,62 @@
+{% load form_helpers %}
+{% load helpers %}
+
+<div class="col col-md-5 col-lg-4 col-xl-3 col-xxl-2 noprint">
+    <form action="." method="get">
+        <div class="card small">
+            <h5 class="card-header">
+                Field Filters
+            </h5>
+            <div class="card-body overflow-visible d-flex flex-wrap justify-content-between py-3">
+                    {% for field in filter_form.hidden_fields %}
+                        {{ field }}
+                    {% endfor %}
+                    {% if filter_form.field_groups %}
+                        {% for group in filter_form.field_groups %}
+                            <div class="col col-12">
+                                {% for name in group %}
+                                    {% with field=filter_form|get_item:name %}
+                                        {% if field|widget_type == 'checkboxinput' %}
+                                            <div class="form-check mb-3">
+                                                <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
+                                                {{ field }}
+                                            </div>
+                                        {% else %}
+                                            <div class="mb-3 mx-3">
+                                                <label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
+                                                {{ field }}
+                                            </div>
+                                        {% endif %}
+                                    {% endwith %}
+                                {% endfor %}
+                            </div>
+                        {% endfor %}
+                    {% else %}
+                        {% for field in filter_form.visible_fields %}
+                            <div class="col">
+                                {% if field|widget_type == 'checkboxinput' %}
+                                    <div class="form-check mb-3">
+                                        <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
+                                        {{ field }}
+                                    </div>
+                                {% else %}
+                                    <div class="mb-3">
+                                    <label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
+                                    {{ field }}
+                                    </div>
+                                {% endif %}
+                            </div>
+                        {% endfor %}
+                    {% endif %}
+            </div>
+            <div class="card-footer text-end noprint border-0">
+                <button type="button" class="btn btn-sm btn-outline-danger m-1" data-reset-select>
+                    <i class="mdi mdi-backspace"></i> Reset
+                </button>
+                <button type="submit" class="btn btn-sm btn-primary m-1">
+                    <i class="mdi mdi-filter-variant"></i> Filter
+                </button>
+            </div>
+        </div>
+    </form>
+</div>

+ 1 - 11
netbox/templates/inc/table_controls.html

@@ -1,6 +1,6 @@
 <div class="row mb-3 justify-content-between">
     <div class="col col-md-2 mb-0 d-flex noprint table-controls">
-        {% if request.user.is_authenticated %}
+        {% if request.user.is_authenticated and table_modal %}
             <div class="input-group input-group-sm">
                 <button
                     type="button"
@@ -22,16 +22,6 @@
                 placeholder="Filter"
                 title="Filter text (regular expressions supported)"
             />
-            {% if filter_form %}
-                <button
-                    type="button"
-                    class="btn btn-sm btn-outline-dark"
-                    data-bs-toggle="collapse"
-                    data-bs-target="#advanced-search-content"
-                >
-                    Advanced Search
-                </button>
-            {% endif %}
         </div>
     </div>
 </div>

+ 8 - 4
netbox/tenancy/forms.py

@@ -67,7 +67,8 @@ class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     parent_id = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         required=False,
-        label=_('Parent group')
+        label=_('Parent group'),
+        fetch_trigger='open'
     )
 
 
@@ -137,7 +138,8 @@ class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
         queryset=TenantGroup.objects.all(),
         required=False,
         null_option='None',
-        label=_('Group')
+        label=_('Group'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -169,7 +171,8 @@ class TenancyFilterForm(forms.Form):
         queryset=TenantGroup.objects.all(),
         required=False,
         null_option='None',
-        label=_('Tenant group')
+        label=_('Tenant group'),
+        fetch_trigger='open'
     )
     tenant_id = DynamicModelMultipleChoiceField(
         queryset=Tenant.objects.all(),
@@ -178,5 +181,6 @@ class TenancyFilterForm(forms.Form):
         query_params={
             'group_id': '$tenant_group_id'
         },
-        label=_('Tenant')
+        label=_('Tenant'),
+        fetch_trigger='open'
     )

+ 12 - 1
netbox/utilities/forms/fields.py

@@ -65,6 +65,7 @@ class SlugField(forms.SlugField):
     """
     Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
     """
+
     def __init__(self, slug_source='name', *args, **kwargs):
         label = kwargs.pop('label', "Slug")
         help_text = kwargs.pop('help_text', "URL-friendly unique shorthand")
@@ -113,6 +114,7 @@ class JSONField(_JSONField):
     """
     Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
     """
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if not self.help_text:
@@ -312,6 +314,7 @@ class ExpandableNameField(forms.CharField):
     A field which allows for numeric range expansion
       Example: 'Gi0/[1-3]' => ['Gi0/1', 'Gi0/2', 'Gi0/3']
     """
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if not self.help_text:
@@ -337,6 +340,7 @@ class ExpandableIPAddressField(forms.CharField):
     A field which allows for expansion of IP address ranges
       Example: '192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24']
     """
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         if not self.help_text:
@@ -363,16 +367,19 @@ class DynamicModelChoiceMixin:
     :param null_option: The string used to represent a null selection (if any)
     :param disabled_indicator: The name of the field which, if populated, will disable selection of the
         choice (optional)
+    :param str fetch_trigger: The event type which will cause the select element to
+        fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
     """
     filter = django_filters.ModelChoiceFilter
     widget = widgets.APISelect
 
-    def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, *args,
+    def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, fetch_trigger=None, *args,
                  **kwargs):
         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
 
         # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
         # by widget_attrs()
@@ -395,6 +402,10 @@ class DynamicModelChoiceMixin:
         if self.disabled_indicator is not None:
             attrs['disabled-indicator'] = self.disabled_indicator
 
+        # Set the fetch trigger, if any.
+        if self.fetch_trigger is not None:
+            attrs['data-fetch-trigger'] = self.fetch_trigger
+
         # Attach any static query parameters
         for key, value in self.query_params.items():
             widget.add_query_param(key, value)

+ 28 - 14
netbox/virtualization/forms.py

@@ -236,12 +236,14 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
     type_id = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         required=False,
-        label=_('Type')
+        label=_('Type'),
+        fetch_trigger='open'
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -250,13 +252,15 @@ class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilte
         query_params={
             'region_id': '$region_id'
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     group_id = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         required=False,
         null_option='None',
-        label=_('Group')
+        label=_('Group'),
+        fetch_trigger='open'
     )
     tag = TagFilterField(model)
 
@@ -547,28 +551,33 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
         queryset=ClusterGroup.objects.all(),
         required=False,
         null_option='None',
-        label=_('Cluster group')
+        label=_('Cluster group'),
+        fetch_trigger='open'
     )
     cluster_type_id = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         required=False,
         null_option='None',
-        label=_('Cluster type')
+        label=_('Cluster type'),
+        fetch_trigger='open'
     )
     cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
-        label=_('Cluster')
+        label=_('Cluster'),
+        fetch_trigger='open'
     )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
-        label=_('Region')
+        label=_('Region'),
+        fetch_trigger='open'
     )
     site_group_id = DynamicModelMultipleChoiceField(
         queryset=SiteGroup.objects.all(),
         required=False,
-        label=_('Site group')
+        label=_('Site group'),
+        fetch_trigger='open'
     )
     site_id = DynamicModelMultipleChoiceField(
         queryset=Site.objects.all(),
@@ -578,7 +587,8 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
             'region_id': '$region_id',
             'group_id': '$site_group_id',
         },
-        label=_('Site')
+        label=_('Site'),
+        fetch_trigger='open'
     )
     role_id = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
@@ -587,7 +597,8 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
         query_params={
             'vm_role': "True"
         },
-        label=_('Role')
+        label=_('Role'),
+        fetch_trigger='open'
     )
     status = forms.MultipleChoiceField(
         choices=VirtualMachineStatusChoices,
@@ -598,7 +609,8 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
         queryset=Platform.objects.all(),
         required=False,
         null_option='None',
-        label=_('Platform')
+        label=_('Platform'),
+        fetch_trigger='open'
     )
     mac_address = forms.CharField(
         required=False,
@@ -850,7 +862,8 @@ class VMInterfaceFilterForm(BootstrapMixin, forms.Form):
     cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         required=False,
-        label=_('Cluster')
+        label=_('Cluster'),
+        fetch_trigger='open'
     )
     virtual_machine_id = DynamicModelMultipleChoiceField(
         queryset=VirtualMachine.objects.all(),
@@ -858,7 +871,8 @@ class VMInterfaceFilterForm(BootstrapMixin, forms.Form):
         query_params={
             'cluster_id': '$cluster_id'
         },
-        label=_('Virtual machine')
+        label=_('Virtual machine'),
+        fetch_trigger='open'
     )
     enabled = forms.NullBooleanField(
         required=False,

Некоторые файлы не были показаны из-за большого количества измененных файлов