Repository: netbox-community/netbox
Pull request: Adds device type and role to device component filter
<!--
    Thank you for your interest in contributing to NetBox! Please note that
    our contribution policy requires that a feature request or bug report be
    approved and assigned prior to opening a pull request. This helps avoid
    waste time and effort on a proposed change that we might not be able to
    accept.

    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ISSUE WHICH HAS BEEN ASSIGNED
    TO YOU, IT WILL BE CLOSED AUTOMATICALLY.

    Please specify your assigned issue number on the line below.
-->
### Fixes: #12015

<!--
    Please include a summary of the proposed changes below.
-->


Problem statement:
Add device type/role to Interface filter
### NetBox version

v3.4.4

### Feature type

New functionality

### Proposed functionality

Add the ability to filter interfaces by device types or device roles. I would like to review a set of devices that all fall under one device type or device role, and check whether a connection has been made to the interface. However, to do this, I have to search for each device individually and select every one into the filter to display what would be the same result if I could just filter by device type/role.

### Use case

The use case for this would be reviewing and checking interfaces easily for specific device types or roles. More specifically, the ability to see cables and their connections for device types and roles.

### Database changes

_No response_

### External dependencies

_No response_

Hints from issue discussion:
If undertaken, this should be implemented consistently for all device components (not just interfaces).
Hi Jeremy, 

Yes I agree, the ability to filter any of the device's components by device roles or device types would be highly beneficial

Commit message: adds device type and role to device component filter #12015

Patch to review:
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index fccaa72f089..e784be8e85b 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -1219,6 +1219,28 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
         to_field_name='name',
         label=_('Device (name)'),
     )
+    device_type_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__device_type',
+        queryset=DeviceType.objects.all(),
+        label=_('Device type (ID)'),
+    )
+    device_type = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__device_type__model',
+        queryset=DeviceType.objects.all(),
+        to_field_name='model',
+        label=_('Device type (model)'),
+    )
+    device_role_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__device_role',
+        queryset=DeviceRole.objects.all(),
+        label=_('Device role (ID)'),
+    )
+    device_role = django_filters.ModelMultipleChoiceFilter(
+        field_name='device__device_role__slug',
+        queryset=DeviceRole.objects.all(),
+        to_field_name='slug',
+        label=_('Device role (slug)'),
+    )
     virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
         field_name='device__virtual_chassis',
         queryset=VirtualChassis.objects.all(),
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index d31bba03053..73a98c61382 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -102,13 +102,25 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
         required=False,
         label=_('Virtual Chassis')
     )
+    device_type_id = DynamicModelMultipleChoiceField(
+        queryset=DeviceType.objects.all(),
+        required=False,
+        label=_('Device type')
+    )
+    device_role_id = DynamicModelMultipleChoiceField(
+        queryset=DeviceRole.objects.all(),
+        required=False,
+        label=_('Device role')
+    )
     device_id = DynamicModelMultipleChoiceField(
         queryset=Device.objects.all(),
         required=False,
         query_params={
             'site_id': '$site_id',
             'location_id': '$location_id',
-            'virtual_chassis_id': '$virtual_chassis_id'
+            'virtual_chassis_id': '$virtual_chassis_id',
+            'device_type_id': '$device_type_id',
+            'role_id': '$device_role_id'
         },
         label=_('Device')
     )
@@ -1070,7 +1082,8 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type', 'speed')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Connection', ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
@@ -1089,7 +1102,8 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type', 'speed')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Connection', ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
@@ -1108,7 +1122,8 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Connection', ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
@@ -1123,7 +1138,8 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Connection', ('cabled', 'connected', 'occupied')),
     )
     type = forms.MultipleChoiceField(
@@ -1141,8 +1157,8 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         ('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
         ('PoE', ('poe_mode', 'poe_type')),
         ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id',
-                    'device_id', 'vdc_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'vdc_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Connection', ('cabled', 'connected', 'occupied')),
     )
     vdc_id = DynamicModelMultipleChoiceField(
@@ -1242,7 +1258,8 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type', 'color')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Cable', ('cabled', 'occupied')),
     )
     model = FrontPort
@@ -1261,7 +1278,8 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type', 'color')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
         ('Cable', ('cabled', 'occupied')),
     )
     type = forms.MultipleChoiceField(
@@ -1279,7 +1297,8 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'position')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
     )
     tag = TagFilterField(model)
     position = forms.CharField(
@@ -1292,7 +1311,8 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
     )
     tag = TagFilterField(model)
 
@@ -1302,7 +1322,8 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
+        ('Device', ('device_type_id', 'device_role_id', 'device_id')),
     )
     role_id = DynamicModelMultipleChoiceField(
         queryset=InventoryItemRole.objects.all(),


Code review comments:
diff_hunk: @@ -1070,7 +1082,8 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
     fieldsets = (
         (None, ('q', 'filter_id', 'tag')),
         ('Attributes', ('name', 'label', 'type', 'speed')),
-        ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id', 'device_id')),
+        ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', 'virtual_chassis_id')),
path: netbox/dcim/forms/filtersets.py
text: @user1:
Should we move `virtual_chassis_id` down to the "Device" group? That seems more fitting IMO.