Repository: netbox-community/netbox
Pull request: Limit amount of nonracked devices displayed
Fixes #8920

Limits the amount of non-racked devices on Site and Location view to 10 and provides a link to the device list this is pre-filtered to the relevant site or location.


Problem statement:
Limit the number of non-racked devices that are listed on the Site and Location pages
### NetBox version

netbox-84cb8b95fd-dcfw8 (v3.2.0-beta2)

### Feature type

Change to existing functionality

### Proposed functionality

Only show a small number (10 or so) of non-racked devices on site and location pages. Include a link that would take you to a device search with pre-populated search that limits the devices shown to that site/location and have not been assigned to a rack.



### Use case

Currently, when you view a site or a location in NetBox, it will list all of the non-racked devices that have been assigned to that site or location. If you have a lot of non-racked devices, the page can take a very long time to generate and is very unwieldy to use. By linking to a device search, you can use the paging functionality or further narrow the search parameters.

### Database changes

None

### External dependencies

None

Hints from issue discussion:
This sounds like it would be addressed by #8184 (allowing the user to paginate through the table without leaving the page).
@jeremystretch Is this FR approved in it's current form or are you suggesting only #8184 could close it?

I'm happy to pick it up in it's current form if so.
@jasonyates both. I think it makes sense in the immediate term to limit the number of devices listed, and once #8184 is in place it will no longer be necessary to do so.

I'll assign this to you, thanks!

Commit message: Update nonracked_devices.html

Fixing issue where Displaying 10 of 10 would appear when it's not required.

Patch to review:
diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py
index c0b46f16e1e..9b24ea9d10d 100644
--- a/netbox/dcim/views.py
+++ b/netbox/dcim/views.py
@@ -346,7 +346,13 @@ def get_extra_context(self, request, instance):
             site=instance,
             position__isnull=True,
             parent_bay__isnull=True
-        ).prefetch_related('device_type__manufacturer')
+        ).prefetch_related('device_type__manufacturer').order_by('-pk')[:10]
+
+        total_nonracked_devices_count = Device.objects.filter(
+            site=instance,
+            position__isnull=True,
+            parent_bay__isnull=True
+        ).count()
 
         asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
         asn_count = asns.count()
@@ -358,6 +364,7 @@ def get_extra_context(self, request, instance):
             'locations': locations,
             'asns': asns,
             'nonracked_devices': nonracked_devices,
+            'total_nonracked_devices_count': total_nonracked_devices_count,
         }
 
 
@@ -439,13 +446,20 @@ def get_extra_context(self, request, instance):
             location=instance,
             position__isnull=True,
             parent_bay__isnull=True
-        ).prefetch_related('device_type__manufacturer')
+        ).prefetch_related('device_type__manufacturer').order_by('-pk')[:10]
+
+        total_nonracked_devices_count = Device.objects.filter(
+            location=instance,
+            position__isnull=True,
+            parent_bay__isnull=True
+        ).count()
 
         return {
             'rack_count': rack_count,
             'device_count': device_count,
             'child_locations_table': child_locations_table,
             'nonracked_devices': nonracked_devices,
+            'total_nonracked_devices_count': total_nonracked_devices_count,
         }
 
 
diff --git a/netbox/templates/dcim/inc/nonracked_devices.html b/netbox/templates/dcim/inc/nonracked_devices.html
index 7f4da2f243e..d4cd588393d 100644
--- a/netbox/templates/dcim/inc/nonracked_devices.html
+++ b/netbox/templates/dcim/inc/nonracked_devices.html
@@ -1,40 +1,54 @@
 {% load helpers %}
 
 <div class="card">
-<h5 class="card-header">
-    Non-Racked Devices
-</h5>
-<div class="card-body">
-{% if nonracked_devices %}
-    <table class="table table-hover">
-        <tr>
-            <th>Name</th>
-            <th>Role</th>
-            <th>Type</th>
-            <th colspan="2">Parent Device</th>
-        </tr>
-        {% for device in nonracked_devices %}
-        <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
-            <td>
-                <a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
-            </td>
-            <td>{{ device.device_role }}</td>
-            <td>{{ device.device_type }}</td>
-            {% if device.parent_bay %}
-                <td>{{ device.parent_bay.device|linkify }}</td>
-                <td>{{ device.parent_bay }}</td>
-            {% else %}
-                <td colspan="2" class="text-muted">&mdash;</td>
+    <h5 class="card-header">
+        Non-Racked Devices
+    </h5>
+    <div class="card-body">
+    {% if nonracked_devices %}
+        <table class="table table-hover">
+            <tr>
+                <th>Name</th>
+                <th>Role</th>
+                <th>Type</th>
+                <th colspan="2">Parent Device</th>
+            </tr>
+            {% for device in nonracked_devices %}
+            <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
+                <td>
+                    <a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
+                </td>
+                <td>{{ device.device_role }}</td>
+                <td>{{ device.device_type }}</td>
+                {% if device.parent_bay %}
+                    <td>{{ device.parent_bay.device|linkify }}</td>
+                    <td>{{ device.parent_bay }}</td>
+                {% else %}
+                    <td colspan="2" class="text-muted">&mdash;</td>
+                {% endif %}
+            </tr>
+            {% endfor %}
+        </table>
+
+        {%  if total_nonracked_devices_count > nonracked_devices.count %}
+            {% if object|meta:'verbose_name' == 'site' %}
+                <div class="text-muted">
+                    Displaying {{ nonracked_devices.count }} of {{ total_nonracked_devices_count }} devices (<a href="{% url 'dcim:device_list' %}?site_id={{ object.pk }}&rack_id=null">View full list</a>)
+                </div>
+            {% elif object|meta:'verbose_name' == 'location' %}
+                <div class="text-muted">
+                    Displaying {{ nonracked_devices.count }} of {{ total_nonracked_devices_count }} devices (<a href="{% url 'dcim:device_list' %}?location_id={{ object.pk }}&rack_id=null">View full list</a>)
+                </div>
             {% endif %}
-        </tr>
-        {% endfor %}
-    </table>
+        {% endif %}
+
     {% else %}
         <div class="text-muted">
             None
         </div>
     {% endif %}
     </div>
+
     {% if perms.dcim.add_device %}
         {% if object|meta:'verbose_name' == 'rack' %}
         <div class="card-footer text-end noprint">


Code review comments:
diff_hunk: @@ -346,7 +346,13 @@ def get_extra_context(self, request, instance):
             site=instance,
             position__isnull=True,
             parent_bay__isnull=True
-        ).prefetch_related('device_type__manufacturer')
+        ).prefetch_related('device_type__manufacturer').order_by('-pk')[:10]
path: netbox/dcim/views.py
text: @user1:
To avoid duplicating the queryset, I suggest declaring it once and then referencing that to get both the limited set (`[:10]`) and the total count. You can leave the queryset as-is here:

```python
        nonracked_devices = Device.objects.filter(
            site=instance,
            position__isnull=True,
            parent_bay__isnull=True
        ).prefetch_related('device_type__manufacturer')
```

And under the template context, do:

```
            'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
            'total_nonracked_devices_count': nonracked_devices.count(),
```

Same idea with LocationView below. This helps ensure that any future changes to the queryset aren't missed in the second definition.

@author:
Neat. I didn't realise you could do that. Updated FR pushed.