From 863048cda229891379881ad2722b70f9d68b63e9 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Sun, 1 Aug 2021 21:24:22 -0700 Subject: [PATCH] Deprecate collapsible advanced search and re-implement field-based filtering on object views --- netbox/circuits/forms.py | 24 ++- netbox/dcim/forms.py | 185 ++++++++++++------ netbox/extras/forms.py | 45 +++-- netbox/ipam/forms.py | 75 ++++--- netbox/project-static/dist/netbox-dark.css | Bin 765466 -> 766359 bytes netbox/project-static/dist/netbox-light.css | Bin 477529 -> 478150 bytes netbox/project-static/dist/netbox.js | Bin 312604 -> 313402 bytes netbox/project-static/dist/netbox.js.map | Bin 1116977 -> 1119658 bytes netbox/project-static/src/select/api.ts | 101 ++++++++-- netbox/project-static/src/select/util.ts | 9 + netbox/project-static/styles/netbox.scss | 12 +- netbox/project-static/styles/select.scss | 31 ++- netbox/templates/dcim/connections_list.html | 42 ++-- .../templates/dcim/rack_elevation_list.html | 97 +++++---- netbox/templates/generic/object_list.html | 14 +- netbox/templates/inc/filter_list.html | 62 ++++++ netbox/templates/inc/table_controls.html | 12 +- netbox/tenancy/forms.py | 12 +- netbox/utilities/forms/fields.py | 9 +- netbox/virtualization/forms.py | 42 ++-- 20 files changed, 523 insertions(+), 249 deletions(-) create mode 100644 netbox/templates/inc/filter_list.html diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 4a86c55e7..261bb8bd6 100644 --- a/netbox/circuits/forms.py +++ b/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, diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 0eb86035e..da9030ca9 100644 --- a/netbox/dcim/forms.py +++ b/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, diff --git a/netbox/extras/forms.py b/netbox/extras/forms.py index 3f3dead0e..16091885c 100644 --- a/netbox/extras/forms.py +++ b/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' ) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 2dc3171e6..f60853525 100644 --- a/netbox/ipam/forms.py +++ b/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) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index ca9ac8d6a6e836b7e29dd9b6ba082eb2f4db2883..8e84ace00847e4edb2ed2cfe444bad6349cc25f4 100644 GIT binary patch delta 442 zcmbPrM{oLLy@nRX7N!>F7M2#)7Pc1lEgZ48reCIbq<&54~}q%ZBMwxAY~i_j1nsY>+;N$k_;=8=@TgjK**v!zi0~JVapK_05gZcCqdpHExbdvLP@{6V~;NdiGuM*<~Vy^8~V%+TV zvU)(7oWz3S%)In!py%?6GfOh_^Q-QYuszL`d#&*F`2B3+(@#jUDo=~#W|{8q N#jUhG#*=$r3IGMcrz-#e delta 93 zcmV-j0HXhw<1U)qE`WpqgaU*Egam{Iga(8Mv!mtsc+BbPr+3ktWrObgm%0h=VT diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index 423470886cfd874c759a9904f693c47a63505273..b657366d753142df907e55ae8e0427179143ec4d 100644 GIT binary patch delta 395 zcmcaPN%q)u*@hOz7N!>F7M3lnJjK)Fi&(j*+sCo8PY)<$RhZsa#40?!AeohAyFf83 zFSBM&W^sv?m2P=zQg&vEZgFx^eojtOVo`NbW_m`6m4S76W=cthl}SOx^!id(ITb8Q ztupex~L3tfy~e zWtX15UXY!G9pXw&kQq|b^V5J{Q&MLZhI84Z6p~6yO7iona}$fwGxKzFQqxMT^vq3* zQgacCru(O}vTfHaW7Sfayt7__O(!`&C%>pYY#QtKuxV^6$+~(#ft^7CYM{gO xiZe?x^Yg4SQb8f3pl4XDkeXPWstYtfzqDj~+)uV9p6T*-EaKZAu(DS=0|3{PiG=_F delta 78 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 delta 28320 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 diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 197b62541f1e3c448e9ac0af083229a7117accd6..cd615818868c6ea5a1b10ddd8d73a546e203278a 100644 GIT binary patch delta 2874 zcmb7FTWB2D8Ag^YOJ_#%Hjd)jGTviwJ6e0R8{sxhB)iR-*_FJ=imc04D6xV^vq#!N zvoqV7ktJDi*M)@^T4+P5|Fn>nLLb`1A()sffy4>1b9pHx(8z5Q8cIWhk9Yp-?GcGo&;fY9EOszVmYuiS}|D3(XyE1aFKJqGgp`%(d0=j0|Hnkipun?m#OLjI-O#@#8 zmp;M+&0QBK5e=prSgsSIlBU`QE%GH})eh89FNlDT;X`oFD`5)fRqDGjlET>{r%OCA zOZ;fD32h?-OsB{_1U5pvVgS8GlKW!Y!;NB%oO9rpw@BY1jMH74=1U&;OT%>0b!~1q z^b`%eRbJn^Ne=b)``qx%(plRudC9fW`Y=^SEj!?zQvdrqWTpKZq8eUggD$QHfpG4| z6&Jb_1vEd`uNP$LB$;Xu6J%O&19;_zsbh@XzaA z9laqQgE2#aAG}MRoDOlM))F1q?&*PsY7ls-6y1zOXcSPxugE=&fT0xPgw0GZVFo@2 zx%KPf4PMfmak#roD(~q5Z={y)PiVvBI|ZL6GK@<702cg@)7rCJOv#wzC&X zmpT)0`LBe)s|9&CB!5d%aOVlRyZ-1O$kU^0#aQMdBaaJD94EpGO73b90&t9eYn6K& zV##!i&*@Xx1oN196T~>d;1{`yi8)I(LV|4396!_d)ft+*yrBQ;R0^6y34U2O6!S zM3dV{p3=>5z2mUQ40@@76Mals3>-y8R^SZ!<;H2b$Nadw(q8PBiV+BKHSEv)Dp+_Q(&x)31<$?>_gLgg+U7J^1UbKlj;Q z|2SiF)9O6SpHXv6XFCX6*3>bk7gU|)hQyIg2vSgI7@JW|#w_uegO6VBIZ$PW5WApn zi%keyk04=Fg3KX9zo@BT$hrs<;g7%@! zDZ!eLQX0Fc&3!coAHJUW^@GPpixCH7-_TI!#AroRIUCOk!c<>l6S}&hv0Qj&YZ^v8 zzOL1;-AsJgCLu#OoMF0C|M44%lWlP8&BVT{DKZ$3h`|suG&APu5kO7Wl!$9tX9Y*c zyr)@&T8KTO_-Le$LIqURzqyU6feby+TG(?ex72b?PO`Dt=x&JO%rX6Ri}nP3{AOb8 zf!v9#T44GZy3U=Bq{GnWM8H^6^p27 zz&}9Phxjy*A^PAC#_~24WDic}WIoK!xd(OZK?KnUK~R%*IMEB|@bR5{?m73~{n(?? z*j3A8F_zczS*+!^>a2iOKS6d)Vaq6K#P}4+VJXZvQ}j$(GYdsc#ktPFCj9xH_=Bq1w;tVg>^NSYtQT>)Gr;~QN)l!s`$`AE2Lh0bG=+0(y5*rYa<3E&(-UNGkUH3*m#u zel{!Y_eA0VDeBN+ngmGJCD>AYPJtBdkU=SS z;lxKF+Pl#x; +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 `` element. + */ + private readonly allowRefresh: boolean = true; + /** * Event to be dispatched when dependent fields' values change. */ @@ -153,6 +179,7 @@ class APISelect { allowDeselect: true, deselectLabel: ``, 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(this.base, 'button[data-reset-select'); + const resetButton = findFirstAdjacent( + 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() { diff --git a/netbox/project-static/src/select/util.ts b/netbox/project-static/src/select/util.ts index e79a233fc..daf7839dc 100644 --- a/netbox/project-static/src/select/util.ts +++ b/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); +} diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 5f395ff7c..6392a03ca 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -52,7 +52,7 @@ } * { - transition: $transition-100ms-ease-in-out; + transition: background-color, color 0.1s ease-in-out; } .mw-25 { @@ -302,8 +302,13 @@ span.profile-button .dropdown-menu { } } -div#advanced-search-content div.card div.card-body div.col:not(:last-child) { - margin-right: 1rem; +div#advanced-search-content { + &.collapsing { + transition: height 0.1s ease-in-out; + } + div.card div.card-body div.col:not(:last-child) { + margin-right: 1rem; + } } body { @@ -430,6 +435,7 @@ nav.search { background-color: var(--nbx-body-bg); // Don't overtake dropdowns z-index: 999; + justify-content: center; form button.dropdown-toggle { border-color: $input-border-color; font-weight: $input-group-addon-font-weight; diff --git a/netbox/project-static/styles/select.scss b/netbox/project-static/styles/select.scss index bc51cb4ea..8b9c51865 100644 --- a/netbox/project-static/styles/select.scss +++ b/netbox/project-static/styles/select.scss @@ -71,8 +71,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 { border-radius: $badge-border-radius; color: var(--nbx-select-value-color); + + // 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-left-radius: $form-select-border-radius; border-bottom-right-radius: $form-select-border-radius; + .ss-search { + padding-right: $spacer * 0.5; + + button { + margin-left: $spacer * 0.75; + } + input[type='search'] { background-color: $form-select-bg; color: $input-color; diff --git a/netbox/templates/dcim/connections_list.html b/netbox/templates/dcim/connections_list.html index f590c4199..cfe17d62f 100644 --- a/netbox/templates/dcim/connections_list.html +++ b/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 %} -
- {% include 'inc/advanced_search.html' %} -
-{% endif %} -
-
-
-
-
-
-
- - {% if filter_form %} - - {% endif %} -
-
-
-
-
- {% include 'inc/responsive_table.html' %} +
+
+ {% include 'inc/table_controls.html' %} + +
+ {% render_table table 'inc/table.html' %}
+ + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} + {% if filter_form %} + {% include 'inc/filter_list.html' %} + {% endif %}
-
{% endblock %} diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index 0e2fbfdad..55e1eeacc 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -5,63 +5,58 @@ {% block title %}Rack Elevations{% endblock %} {% block controls %} -
-
- - -
- Front - Rear -
-
- Normal - Reversed +
+
+ +
+ Front + Rear +
+
-
{% endblock %} {% block content %} -
- {% include 'inc/advanced_search.html' %} -
-
-
- {% if page %} -
- {% for rack in page %} -
-
-
- {{ rack.name }} - {% if rack.role %} -
{{ rack.role }} - {% endif %} - {% if rack.facility_id %} -
{{ rack.facility_id }} - {% endif %} +
+
+ {% if page %} +
+ {% for rack in page %} +
+
+
+ {{ rack.name }} + {% if rack.role %} +
{{ rack.role }} + {% endif %} + {% if rack.facility_id %} +
{{ rack.facility_id }} + {% endif %} +
+ {% include 'dcim/inc/rack_elevation.html' with object=rack face=rack_face %} +
+
+ {{ rack.name }} + {% if rack.facility_id %} + ({{ rack.facility_id }}) + {% endif %} +
- {% include 'dcim/inc/rack_elevation.html' with object=rack face=rack_face %} -
-
- {{ rack.name }} - {% if rack.facility_id %} - ({{ rack.facility_id }}) - {% endif %}
-
-
- {% endfor %} -
-
- {% include 'inc/paginator.html' %} - {% else %} -

No Racks Found

- {% endif %} + {% endfor %} +
+
+ {% include 'inc/paginator.html' %} + {% else %} +

No Racks Found

+ {% endif %} +
+ {% include 'inc/filter_list.html' %}
-
{% endblock %} diff --git a/netbox/templates/generic/object_list.html b/netbox/templates/generic/object_list.html index e6ae37403..9ac484227 100644 --- a/netbox/templates/generic/object_list.html +++ b/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" %}
@@ -57,12 +54,12 @@ {% endwith %} {% endif %} -{# Object list filter, table config #} -{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %} - {# Object table #}
-
+
+ {# 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 %}
@@ -95,6 +92,9 @@ {% endwith %} {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+ {% if filter_form %} + {% include 'inc/filter_list.html' %} + {% endif %}
{% table_config_form table table_name="ObjectTable" %} {% endblock content %} diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html new file mode 100644 index 000000000..07aa2249b --- /dev/null +++ b/netbox/templates/inc/filter_list.html @@ -0,0 +1,62 @@ +{% load form_helpers %} +{% load helpers %} + +
+ +
+
+ Field Filters +
+
+ {% for field in filter_form.hidden_fields %} + {{ field }} + {% endfor %} + {% if filter_form.field_groups %} + {% for group in filter_form.field_groups %} +
+ {% for name in group %} + {% with field=filter_form|get_item:name %} + {% if field|widget_type == 'checkboxinput' %} +
+ + {{ field }} +
+ {% else %} +
+ + {{ field }} +
+ {% endif %} + {% endwith %} + {% endfor %} +
+ {% endfor %} + {% else %} + {% for field in filter_form.visible_fields %} +
+ {% if field|widget_type == 'checkboxinput' %} +
+ + {{ field }} +
+ {% else %} +
+ + {{ field }} +
+ {% endif %} +
+ {% endfor %} + {% endif %} +
+ +
+ +
diff --git a/netbox/templates/inc/table_controls.html b/netbox/templates/inc/table_controls.html index bf604ab27..ec46cd535 100644 --- a/netbox/templates/inc/table_controls.html +++ b/netbox/templates/inc/table_controls.html @@ -1,6 +1,6 @@
- {% if request.user.is_authenticated %} + {% if request.user.is_authenticated and table_modal %}
- {% endif %}
diff --git a/netbox/tenancy/forms.py b/netbox/tenancy/forms.py index 19e2fc38c..05b47ad4c 100644 --- a/netbox/tenancy/forms.py +++ b/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' ) diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py index b1db79ecc..9ea4ddb81 100644 --- a/netbox/utilities/forms/fields.py +++ b/netbox/utilities/forms/fields.py @@ -363,16 +363,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() @@ -394,6 +397,10 @@ class DynamicModelChoiceMixin: # Set the disabled indicator, if any 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(): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 332bebe8e..7cef7434b 100644 --- a/netbox/virtualization/forms.py +++ b/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,