Repository: netbox-community/netbox
Pull request: Adds generic object children template
<!--
    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: #12110

<!--
    Please include a summary of the proposed changes below.
-->


Problem statement:
Add a generic view_tab.html template to ease plugin development
### NetBox version

v3.4.7

### Feature type

Change to existing functionality

### Proposed functionality

Add `view_tab.html` to `templates/inc` with the following minimalistic content

```
{% extends 'generic/object.html' %}
{% load helpers %}

{% block content %}
    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
    <form method="post">
        {% csrf_token %}
        <div class="card">
            <div class="card-body" id="object_list">
                {% include 'htmx/table.html' %}
            </div>
        </div>
    </form>
{% endblock content %}

{% block modals %}
    {{ block.super }}
    {% table_config_form table %}
{% endblock modals %}
```

To allow `table_config` to be passed down to the template, make the following change in the python code

```
class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
     table = None
     filterset = None
     template_name = 'inc/view_tabl.html'
 
+    def get_extra_context(self, request, instance):
+        return {
+            'table_config': f'{self.table.__name__}_config'
+        }
+
     def get_children(self, request, parent):
         """
         Return a QuerySet of child objects.

```

### Use case

In a lot of my plugins, wherever I use the `ViewTab` functionality, I have to ship out my own template along with the code. In 99% cases, it's exactly the same file which I even reuse within the same plugin to avoid writing multiple ones for different classes. It would ease with the plugin development, if NetBox provided a generic template that the code will automatically look for if not specified by the plugin developer and use that.

### Database changes

_No response_

### External dependencies

_No response_

Hints from issue discussion:
IMO the only way it makes sense for us to add a template is if it can be utilized for core views as well. For ObjectChildrenView specifically, templates will typically inherit from a model-specific parent template anyway so I'm not sure how useful this would be.
I think this can be tackled on how I did for `ObjectContactView` template. I can provide a PR for this on a similar work and I hope we can simplify most of the templates in use.
I don't see how useful a table would be for a "Tab View", IMO you would use ObjectChildrenView for that as it would likely be a related table or similar.

I think it would make sense if there was a generic view and it consisted of a single left pane which simply loops through all the models meta fields (perhaps have a base call to get_extra_content or similar which populates the attribute fields you want to display.
80% of my plugins use tab view to display related data in a table and all of them ship with the exact code to do this including the template file.

So, the idea was to reduce the code by introducing a boilerplate template which can be used by the core and plugins.

Since, there are cases where a template a template would need to provide extra functionality, we can provide blocks to do so.

I think I missed the part where this was for ObjectChildrenView specifically, the mention of "View Tab" threw me off.

Commit message: adds generic tab view template #12110

Patch to review:
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 32badd2d503..5f3046c6aad 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -216,7 +216,6 @@ class ASNRangeASNsView(generic.ObjectChildrenView):
     child_model = ASN
     table = tables.ASNTable
     filterset = filtersets.ASNFilterSet
-    template_name = 'ipam/asnrange/asns.html'
     tab = ViewTab(
         label=_('ASNs'),
         badge=lambda x: x.get_child_asns().count(),
@@ -367,7 +366,9 @@ def prep_table_data(self, request, queryset, parent):
         return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
 
     def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
         return {
+            **context,
             'bulk_querystring': f'within={instance.prefix}',
             'first_available_prefix': instance.get_first_available_prefix(),
             'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
@@ -554,7 +555,9 @@ def prep_table_data(self, request, queryset, parent):
         return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
 
     def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
         return {
+            **context,
             'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}",
             'first_available_prefix': instance.get_first_available_prefix(),
             'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
@@ -582,7 +585,9 @@ def get_children(self, request, parent):
         )
 
     def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
         return {
+            **context,
             'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
             'first_available_ip': instance.get_first_available_ip(),
         }
@@ -611,7 +616,9 @@ def prep_table_data(self, request, queryset, parent):
         return queryset
 
     def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
         return {
+            **context,
             'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
             'first_available_ip': instance.get_first_available_ip(),
         }
@@ -816,7 +823,6 @@ def post(self, request):
         table = None
 
         if form.is_valid():
-
             addresses = self.queryset.prefetch_related('vrf', 'tenant')
             # Limit to 100 results
             addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
@@ -866,7 +872,6 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
     child_model = IPAddress
     table = tables.IPAddressTable
     filterset = filtersets.IPAddressFilterSet
-    template_name = 'ipam/ipaddress/ip_addresses.html'
     tab = ViewTab(
         label=_('Related IPs'),
         badge=lambda x: x.get_related_ips().count(),
@@ -963,7 +968,6 @@ class FHRPGroupView(generic.ObjectView):
     queryset = FHRPGroup.objects.all()
 
     def get_extra_context(self, request, instance):
-
         # Get assigned interfaces
         members_table = tables.FHRPGroupAssignmentTable(
             data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
@@ -1077,7 +1081,6 @@ class VLANInterfacesView(generic.ObjectChildrenView):
     child_model = Interface
     table = tables.VLANDevicesTable
     filterset = InterfaceFilterSet
-    template_name = 'ipam/vlan/interfaces.html'
     tab = ViewTab(
         label=_('Device Interfaces'),
         badge=lambda x: x.get_interfaces().count(),
@@ -1095,7 +1098,6 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
     child_model = VMInterface
     table = tables.VLANVirtualMachinesTable
     filterset = VMInterfaceFilterSet
-    template_name = 'ipam/vlan/vminterfaces.html'
     tab = ViewTab(
         label=_('VM Interfaces'),
         badge=lambda x: x.get_vminterfaces().count(),
diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py
index 1ba789cf187..af7b3bab3c0 100644
--- a/netbox/netbox/views/generic/object_views.py
+++ b/netbox/netbox/views/generic/object_views.py
@@ -6,6 +6,7 @@
 from django.db.models import ProtectedError
 from django.shortcuts import redirect, render
 from django.urls import reverse
+from django.urls.exceptions import NoReverseMatch
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 
@@ -90,6 +91,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
     child_model = None
     table = None
     filterset = None
+    template_name = 'inc/tab_view.html'
 
     def get_children(self, request, parent):
         """
@@ -112,6 +114,28 @@ def prep_table_data(self, request, queryset, parent):
         """
         return queryset
 
+    def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
+
+        return_url = '?return_url=' + request.get_full_path()
+        bulk_edit_url = reverse(f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_edit') + return_url
+        bulk_delete_url = reverse(f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_delete') + return_url
+
+        try:
+            bulk_rename_url = reverse(
+                f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_rename') + return_url
+        except NoReverseMatch:
+            bulk_rename_url = None
+
+        context.update({
+            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
+            'table_config': f'{self.table.__name__}_config',
+            'bulk_edit_url': bulk_edit_url,
+            'bulk_delete_url': bulk_delete_url,
+            'bulk_rename_url': bulk_rename_url,
+        })
+        return context
+
     #
     # Request handlers
     #
diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html
index ccd12f61c87..518ecf2a475 100644
--- a/netbox/templates/dcim/device/consoleports.html
+++ b/netbox/templates/dcim/device/consoleports.html
@@ -1,57 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        <div class="btn-group" role="group">
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_consoleport %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_consoleserverport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Port
-          </a>
+            <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
+    {% endif %}
 {% endblock %}
 
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
+            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html
index 43396651d2a..45f99f5b662 100644
--- a/netbox/templates/dcim/device/consoleserverports.html
+++ b/netbox/templates/dcim/device/consoleserverports.html
@@ -1,57 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
+            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+    {% endif %}
+{% endblock %}
 
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        <div class="btn-group" role="group">
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_consoleserverport %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_consoleserverport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
-          </a>
+            <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html
index 9453b9a59a6..92711909cf4 100644
--- a/netbox/templates/dcim/device/devicebays.html
+++ b/netbox/templates/dcim/device/devicebays.html
@@ -1,50 +1,12 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_devicebay %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_devicebay %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
-          </a>
+            <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
+    {% endif %}
 {% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html
index dd0767d954f..eb4f96ca516 100644
--- a/netbox/templates/dcim/device/frontports.html
+++ b/netbox/templates/dcim/device/frontports.html
@@ -1,57 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
+            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+    {% endif %}
+{% endblock %}
 
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        <div class="btn-group" role="group">
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_frontport %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_frontport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
-          </a>
+            <a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html
index c0e9a38b63d..9d7d28d8a68 100644
--- a/netbox/templates/dcim/device/interfaces.html
+++ b/netbox/templates/dcim/device/interfaces.html
@@ -1,66 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'dcim/device/inc/interface_table_controls.html' with table_modal="DeviceInterfaceTable_config" %}
-
-<form method="post">
-  {% csrf_token %}
-
-  <div class="card">
-    <div class="card-body htmx-container table-responsive" id="object_list">
-      {% include 'htmx/table.html' %}
-    </div>
-  </div>
-
-  <div class="noprint bulk-buttons">
-    <div class="bulk-button-group">
-      {% if 'bulk_edit' in actions %}
-        <div class="btn-group" role="group">
-          <button type="submit" name="_edit"
-            formaction="{% url 'dcim:interface_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-            class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-          <button type="submit" name="_rename"
-            formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-            class="btn btn-outline-warning btn-sm">
-            <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-          </button>
-        </div>
-      {% endif %}
-      <div class="btn-group" role="group">
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete"
-            formaction="{% url 'dcim:interface_bulk_delete' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-            class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_disconnect"
-            formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-            class="btn btn-outline-danger btn-sm">
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:interface_bulk_disconnect' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
             <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-          </button>
-        {% endif %}
-      </div>
-    </div>
-    {% if perms.dcim.add_interface %}
-      <div class="bulk-button-group">
-        <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
-          class="btn btn-primary btn-sm">
-          <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
-        </a>
-      </div>
+        </button>
     {% endif %}
-  </div>
-</form>
 {% endblock %}
 
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_interface %}
+        <div class="bulk-button-group">
+            <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
+            </a>
+        </div>
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html
index 9e11031ec13..ef4071ca9b3 100644
--- a/netbox/templates/dcim/device/inventory.html
+++ b/netbox/templates/dcim/device/inventory.html
@@ -1,50 +1,12 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_inventoryitem %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_inventoryitem %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
-          </a>
+            <a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
+    {% endif %}
 {% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/dcim/device/modulebays.html b/netbox/templates/dcim/device/modulebays.html
index 7f0aacf1f2a..d24755c20a4 100644
--- a/netbox/templates/dcim/device/modulebays.html
+++ b/netbox/templates/dcim/device/modulebays.html
@@ -1,46 +1,12 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceModuleBayTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:modulebay_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:modulebay_bulk_rename' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" formaction="{% url 'dcim:modulebay_bulk_delete' %}?return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_modulebay %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_modulebay %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
-          </a>
+            <a href="{% url 'dcim:modulebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_modulebays' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Module Bays
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-  {% table_config_form table %}
-{% endblock %}
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html
index 66b21b7afbe..58d69a5cce8 100644
--- a/netbox/templates/dcim/device/poweroutlets.html
+++ b/netbox/templates/dcim/device/poweroutlets.html
@@ -1,57 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
+            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+    {% endif %}
+{% endblock %}
 
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        <div class="btn-group" role="group">
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_poweroutlet %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_poweroutlet %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
-          </a>
+            <a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
+    {% endif %}
 {% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html
index d9e1e121a30..f17c81e6d68 100644
--- a/netbox/templates/dcim/device/powerports.html
+++ b/netbox/templates/dcim/device/powerports.html
@@ -1,57 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-{% load static %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
+            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+    {% endif %}
+{% endblock %}
 
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        <div class="btn-group" role="group">
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_powerport %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_powerport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
-          </a>
+            <a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}"
+               class="btn btn-sm btn-primary">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html
index ce194cc7879..dcee920b531 100644
--- a/netbox/templates/dcim/device/rearports.html
+++ b/netbox/templates/dcim/device/rearports.html
@@ -1,57 +1,22 @@
-{% extends 'dcim/device/base.html' %}
-{% load render_table from django_tables2 %}
-{% load static %}
-{% load helpers %}
+{% extends 'inc/tab_view.html' %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
+{% block bulk_delete_control %}
+    {% if 'bulk_edit' in actions %}
+        <button type="submit" name="_disconnect"
+                formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}"
+                class="btn btn-outline-danger btn-sm">
+            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+        </button>
+    {% endif %}
+{% endblock %}
 
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <div class="btn-group" role="group">
-            <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-              <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-          </div>
-        {% endif %}
-        <div class="btn-group" role="group">
-          {% if 'bulk_delete' in actions %}
-            <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-          {% endif %}
-          {% if 'bulk_edit' in actions %}
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-          {% endif %}
-        </div>
-      </div>
-      {% if perms.dcim.add_rearport %}
+{% block extra_contols_bottom %}
+    {% if perms.dcim.add_rearport %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
-          </a>
+            <a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
+    {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/templates/dcim/rack/non_racked_devices.html b/netbox/templates/dcim/rack/non_racked_devices.html
index 700c663698d..ee3736f0340 100644
--- a/netbox/templates/dcim/rack/non_racked_devices.html
+++ b/netbox/templates/dcim/rack/non_racked_devices.html
@@ -1,4 +1,4 @@
-{% extends 'dcim/rack/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -11,41 +11,3 @@
         </div>
     {% endif %}
 {% endblock %}
-
-{% block content %}
-    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
-
-    <form method="post">
-        {% csrf_token %}
-
-        <div class="card">
-            <div class="card-body htmx-container table-responsive" id="object_list">
-                {% include 'htmx/table.html' %}
-            </div>
-        </div>
-
-        <div class="noprint bulk-buttons">
-            <div class="bulk-button-group">
-                {% if 'bulk_edit' in actions %}
-                    <button type="submit" name="_edit"
-                            formaction="{% url 'dcim:device_bulk_edit' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
-                            class="btn btn-warning btn-sm">
-                        <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-                    </button>
-                {% endif %}
-                {% if 'bulk_delete' in actions %}
-                    <button type="submit"
-                            formaction="{% url 'dcim:device_bulk_delete' %}?return_url={% url 'dcim:rack_nonracked_devices' pk=object.pk %}"
-                            class="btn btn-danger btn-sm">
-                        <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-                    </button>
-                {% endif %}
-            </div>
-        </div>
-    </form>
-{% endblock content %}
-
-{% block modals %}
-    {{ block.super }}
-    {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/dcim/rack/reservations.html b/netbox/templates/dcim/rack/reservations.html
index fb357e592ad..e4d4b8dfb0f 100644
--- a/netbox/templates/dcim/rack/reservations.html
+++ b/netbox/templates/dcim/rack/reservations.html
@@ -1,43 +1,13 @@
-{% extends 'dcim/rack/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="RackReservationTable_config" %}
-
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'dcim:rackreservation_bulk_edit' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" formaction="{% url 'dcim:rackreservation_bulk_delete' %}?return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-      {% if perms.dcim.add_rackreservation %}
+{% block extra_controls %}
+    {% if perms.dcim.add_rackreservation %}
         <div class="bulk-button-group">
-          <a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
-          </a>
+            <a href="{% url 'dcim:rackreservation_add' %}?rack={{ object.pk }}&return_url={% url 'dcim:rack_reservations' pk=object.pk %}"
+               class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add reservation
+            </a>
         </div>
-      {% endif %}
-    </div>
-  </form>
+    {% endif %}
 {% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/inc/tab_view.html b/netbox/templates/inc/tab_view.html
new file mode 100644
index 00000000000..12c65f041f8
--- /dev/null
+++ b/netbox/templates/inc/tab_view.html
@@ -0,0 +1,55 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
+    <form method="post">
+        {% csrf_token %}
+        <div class="card">
+            <div class="card-body htmx-container table-responsive" id="object_list">
+                {% include 'htmx/table.html' %}
+            </div>
+        </div>
+
+        {% block bulk_controls %}
+            <div class="noprint bulk-buttons">
+                <div class="bulk-button-group">
+                    {% if 'bulk_edit' in actions %}
+                        <div class="btn-group" role="group">
+                            <button type="submit" name="_edit"
+                                    formaction="{{ bulk_edit_url }}"
+                                    class="btn btn-warning btn-sm">
+                                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                            </button>
+                            {% if bulk_rename_url %}
+                                <button type="submit" name="_rename"
+                                        formaction="{{ bulk_rename_url }}"
+                                        class="btn btn-outline-warning btn-sm">
+                                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                                </button>
+                            {% endif %}
+                        </div>
+                    {% endif %}
+                    <div class="btn-group" role="group">
+                        {% if 'bulk_delete' in actions %}
+                            <button type="submit"
+                                    formaction="{{ bulk_delete_url }}"
+                                    class="btn btn-danger btn-sm">
+                                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                            </button>
+                        {% endif %}
+                        {% block bulk_delete_control %}
+                        {% endblock %}
+                    </div>
+                </div>
+                {% block extra_contols_bottom %}
+                {% endblock %}
+            </div>
+        {% endblock %}
+    </form>
+{% endblock content %}
+
+{% block modals %}
+    {{ block.super }}
+    {% table_config_form table %}
+{% endblock modals %}
diff --git a/netbox/templates/ipam/aggregate/prefixes.html b/netbox/templates/ipam/aggregate/prefixes.html
index a1d3bd2763c..e93d0cf42e9 100644
--- a/netbox/templates/ipam/aggregate/prefixes.html
+++ b/netbox/templates/ipam/aggregate/prefixes.html
@@ -1,4 +1,4 @@
-{% extends 'ipam/aggregate/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -10,37 +10,3 @@
   {% endif %}
   {{ block.super }}
 {% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:aggregate_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/asnrange/asns.html b/netbox/templates/ipam/asnrange/asns.html
deleted file mode 100644
index 69d4e8abb85..00000000000
--- a/netbox/templates/ipam/asnrange/asns.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{% extends 'ipam/asnrange/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="ASNTable_config" %}
-
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:asn_bulk_edit' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:asn_bulk_delete' %}?return_url={% url 'ipam:asnrange_asns' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/ipaddress/ip_addresses.html b/netbox/templates/ipam/ipaddress/ip_addresses.html
deleted file mode 100644
index b82ec2375cb..00000000000
--- a/netbox/templates/ipam/ipaddress/ip_addresses.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends 'ipam/ipaddress/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/iprange/ip_addresses.html b/netbox/templates/ipam/iprange/ip_addresses.html
index 9f77f6c78e9..9acb6a1fbd7 100644
--- a/netbox/templates/ipam/iprange/ip_addresses.html
+++ b/netbox/templates/ipam/iprange/ip_addresses.html
@@ -1,4 +1,4 @@
-{% extends 'ipam/iprange/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -8,37 +8,3 @@
     </a>
   {% endif %}
 {% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/prefix/ip_addresses.html b/netbox/templates/ipam/prefix/ip_addresses.html
index fe68039f8f1..f734e06cc54 100644
--- a/netbox/templates/ipam/prefix/ip_addresses.html
+++ b/netbox/templates/ipam/prefix/ip_addresses.html
@@ -1,4 +1,4 @@
-{% extends 'ipam/prefix/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -8,37 +8,3 @@
     </a>
   {% endif %}
 {% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/prefix/ip_ranges.html b/netbox/templates/ipam/prefix/ip_ranges.html
index 4452fd5a7be..2b2d021dac6 100644
--- a/netbox/templates/ipam/prefix/ip_ranges.html
+++ b/netbox/templates/ipam/prefix/ip_ranges.html
@@ -1,4 +1,4 @@
-{% extends 'ipam/prefix/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -8,37 +8,3 @@
     </a>
   {% endif %}
 {% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:iprange_bulk_edit' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:iprange_bulk_delete' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/prefix/prefixes.html b/netbox/templates/ipam/prefix/prefixes.html
index 5fc931f74de..eafcb6b4701 100644
--- a/netbox/templates/ipam/prefix/prefixes.html
+++ b/netbox/templates/ipam/prefix/prefixes.html
@@ -1,4 +1,4 @@
-{% extends 'ipam/prefix/base.html' %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -10,37 +10,3 @@
   {% endif %}
   {{ block.super }}
 {% endblock %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/vlan/interfaces.html b/netbox/templates/ipam/vlan/interfaces.html
deleted file mode 100644
index f7bcc85630d..00000000000
--- a/netbox/templates/ipam/vlan/interfaces.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends 'ipam/vlan/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/ipam/vlan/vminterfaces.html b/netbox/templates/ipam/vlan/vminterfaces.html
deleted file mode 100644
index a485b33ebbf..00000000000
--- a/netbox/templates/ipam/vlan/vminterfaces.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends 'ipam/vlan/base.html' %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/tenancy/object_contacts.html b/netbox/templates/tenancy/object_contacts.html
index e13fedc4384..9237cbe67a8 100644
--- a/netbox/templates/tenancy/object_contacts.html
+++ b/netbox/templates/tenancy/object_contacts.html
@@ -1,4 +1,4 @@
-{% extends base_template %}
+{% extends 'inc/tab_view.html' %}
 {% load helpers %}
 
 {% block extra_controls %}
@@ -10,20 +10,3 @@
     {% endwith %}
   {% endif %}
 {% endblock %}
-
-{% block content %}
-    {% include 'inc/table_controls_htmx.html' with table_modal="ContactAssignmentTable_config" %}
-    <form method="post">
-        {% csrf_token %}
-        <div class="card">
-            <div class="card-body" id="object_list">
-                {% include 'htmx/table.html' %}
-            </div>
-        </div>
-    </form>
-{% endblock content %}
-
-{% block modals %}
-    {{ block.super }}
-    {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/virtualization/cluster/devices.html b/netbox/templates/virtualization/cluster/devices.html
deleted file mode 100644
index 0837982332e..00000000000
--- a/netbox/templates/virtualization/cluster/devices.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends 'virtualization/cluster/base.html' %}
-{% load helpers %}
-{% load render_table from django_tables2 %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
-  
-  <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if perms.virtualization.change_cluster %}
-          <button type="submit" name="_remove" class="btn btn-danger btn-sm">
-            <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove Devices
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/virtualization/cluster/virtual_machines.html b/netbox/templates/virtualization/cluster/virtual_machines.html
deleted file mode 100644
index 79c489d6bd2..00000000000
--- a/netbox/templates/virtualization/cluster/virtual_machines.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{% extends 'virtualization/cluster/base.html' %}
-{% load helpers %}
-{% load render_table from django_tables2 %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-    <div class="noprint bulk-buttons">
-      <div class="bulk-button-group">
-        {% if 'bulk_edit' in actions %}
-          <button type="submit" name="_edit" formaction="{% url 'virtualization:virtualmachine_bulk_edit' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-          </button>
-        {% endif %}
-        {% if 'bulk_delete' in actions %}
-          <button type="submit" name="_delete" formaction="{% url 'virtualization:virtualmachine_bulk_delete' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-danger btn-sm">
-            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-          </button>
-        {% endif %}
-      </div>
-    </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/templates/virtualization/virtualmachine/interfaces.html b/netbox/templates/virtualization/virtualmachine/interfaces.html
deleted file mode 100644
index 71456d1042e..00000000000
--- a/netbox/templates/virtualization/virtualmachine/interfaces.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% extends 'virtualization/virtualmachine/base.html' %}
-{% load render_table from django_tables2 %}
-{% load helpers %}
-
-{% block content %}
-  {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
-  
-  <form method="post">
-    {% csrf_token %}
-
-    <div class="card">
-      <div class="card-body htmx-container table-responsive" id="object_list">
-        {% include 'htmx/table.html' %}
-      </div>
-    </div>
-
-    <div class="noprint">
-      {% if perms.virtualization.change_vminterface %}
-        <div class="btn-group" role="group">
-          <button type="submit" name="_edit" formaction="{% url 'virtualization:vminterface_bulk_edit' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">
-            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
-          </button>
-          <button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-            <span class="mdi mdi-pencil" aria-hidden="true"></span> Rename
-          </button>
-        </div>
-      {% endif %}
-      {% if perms.virtualization.delete_vminterface %}
-        <button type="submit" name="_delete" formaction="{% url 'virtualization:vminterface_bulk_delete' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-danger btn-sm">
-          <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete
-        </button>
-      {% endif %}
-      {% if perms.virtualization.add_vminterface %}
-        <div class="float-end">
-          <a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add interfaces
-          </a>
-        </div>
-      {% endif %}
-     </div>
-  </form>
-{% endblock content %}
-
-{% block modals %}
-  {{ block.super }}
-  {% table_config_form table %}
-{% endblock modals %}
diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py
index 23020e79423..3025e7e04ed 100644
--- a/netbox/tenancy/views.py
+++ b/netbox/tenancy/views.py
@@ -41,11 +41,6 @@ def get_table(self, *args, **kwargs):
 
         return table
 
-    def get_extra_context(self, request, instance):
-        return {
-            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
-        }
-
 #
 # Tenant groups
 #
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index 75e83f9e1ac..6e177336895 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -175,7 +175,6 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
     child_model = VirtualMachine
     table = tables.VirtualMachineTable
     filterset = filtersets.VirtualMachineFilterSet
-    template_name = 'virtualization/cluster/virtual_machines.html'
     tab = ViewTab(
         label=_('Virtual Machines'),
         badge=lambda obj: obj.virtual_machines.count(),
@@ -193,7 +192,6 @@ class ClusterDevicesView(generic.ObjectChildrenView):
     child_model = Device
     table = DeviceTable
     filterset = DeviceFilterSet
-    template_name = 'virtualization/cluster/devices.html'
     tab = ViewTab(
         label=_('Devices'),
         badge=lambda obj: obj.devices.count(),
@@ -346,7 +344,6 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
     child_model = VMInterface
     table = tables.VirtualMachineVMInterfaceTable
     filterset = filtersets.VMInterfaceFilterSet
-    template_name = 'virtualization/virtualmachine/interfaces.html'
     tab = ViewTab(
         label=_('Interfaces'),
         badge=lambda obj: obj.interfaces.count(),


Code review comments:
path: netbox/templates/inc/tab_view.html
text: @user1:
We should move this from `inc/` to `generic/`. (`inc/` is for partial templates brought in via `{% include %}`.)

diff_hunk: @@ -0,0 +1,55 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
+    <form method="post">
+        {% csrf_token %}
+        <div class="card">
+            <div class="card-body htmx-container table-responsive" id="object_list">
+                {% include 'htmx/table.html' %}
+            </div>
+        </div>
+
+        {% block bulk_controls %}
+            <div class="noprint bulk-buttons">
+                <div class="bulk-button-group">
+                    {% if 'bulk_edit' in actions %}
+                        <div class="btn-group" role="group">
+                            <button type="submit" name="_edit"
+                                    formaction="{{ bulk_edit_url }}"
+                                    class="btn btn-warning btn-sm">
+                                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                            </button>
+                            {% if bulk_rename_url %}
+                                <button type="submit" name="_rename"
+                                        formaction="{{ bulk_rename_url }}"
+                                        class="btn btn-outline-warning btn-sm">
+                                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                                </button>
+                            {% endif %}
+                        </div>
+                    {% endif %}
+                    <div class="btn-group" role="group">
+                        {% if 'bulk_delete' in actions %}
+                            <button type="submit"
+                                    formaction="{{ bulk_delete_url }}"
+                                    class="btn btn-danger btn-sm">
+                                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                            </button>
+                        {% endif %}
path: netbox/templates/inc/tab_view.html
text: @user1:
```suggestion
                        {% with bulk_delete_url=child_model|validated_viewname:"bulk_delete" %}
                            {% if 'bulk_delete' in actions and bulk_delete_url %}
                                <button type="submit"
                                        formaction="{{ bulk_delete_url }}"
                                        class="btn btn-danger btn-sm">
                                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
                                </button>
                            {% endif %}
                        {% endwith %}
```

diff_hunk: @@ -0,0 +1,55 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
+    <form method="post">
+        {% csrf_token %}
+        <div class="card">
+            <div class="card-body htmx-container table-responsive" id="object_list">
+                {% include 'htmx/table.html' %}
+            </div>
+        </div>
+
+        {% block bulk_controls %}
+            <div class="noprint bulk-buttons">
+                <div class="bulk-button-group">
+                    {% if 'bulk_edit' in actions %}
+                        <div class="btn-group" role="group">
+                            <button type="submit" name="_edit"
+                                    formaction="{{ bulk_edit_url }}"
+                                    class="btn btn-warning btn-sm">
+                                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                            </button>
+                            {% if bulk_rename_url %}
+                                <button type="submit" name="_rename"
+                                        formaction="{{ bulk_rename_url }}"
+                                        class="btn btn-outline-warning btn-sm">
+                                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                                </button>
+                            {% endif %}
+                        </div>
+                    {% endif %}
+                    <div class="btn-group" role="group">
+                        {% if 'bulk_delete' in actions %}
+                            <button type="submit"
+                                    formaction="{{ bulk_delete_url }}"
+                                    class="btn btn-danger btn-sm">
+                                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                            </button>
+                        {% endif %}
+                        {% block bulk_delete_control %}
path: netbox/templates/inc/tab_view.html
text: @user1:
We should avoid adding bespoke template blocks like this. They can quickly become very difficult to manage.

@author:
I can remove this and add the `Disconnect` button just like `Rename` but I dont like that button to be in this view as it's not a standard button throughout the application.

diff_hunk: @@ -0,0 +1,55 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
+    <form method="post">
+        {% csrf_token %}
+        <div class="card">
+            <div class="card-body htmx-container table-responsive" id="object_list">
+                {% include 'htmx/table.html' %}
+            </div>
+        </div>
+
+        {% block bulk_controls %}
+            <div class="noprint bulk-buttons">
+                <div class="bulk-button-group">
+                    {% if 'bulk_edit' in actions %}
+                        <div class="btn-group" role="group">
+                            <button type="submit" name="_edit"
+                                    formaction="{{ bulk_edit_url }}"
+                                    class="btn btn-warning btn-sm">
+                                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                            </button>
+                            {% if bulk_rename_url %}
+                                <button type="submit" name="_rename"
+                                        formaction="{{ bulk_rename_url }}"
+                                        class="btn btn-outline-warning btn-sm">
+                                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                                </button>
+                            {% endif %}
+                        </div>
+                    {% endif %}
+                    <div class="btn-group" role="group">
+                        {% if 'bulk_delete' in actions %}
+                            <button type="submit"
+                                    formaction="{{ bulk_delete_url }}"
+                                    class="btn btn-danger btn-sm">
+                                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                            </button>
+                        {% endif %}
+                        {% block bulk_delete_control %}
+                        {% endblock %}
+                    </div>
+                </div>
+                {% block extra_contols_bottom %}
path: netbox/templates/inc/tab_view.html
text: @user1:
We should be able to refactor the parent block a bit so that inheriting templates can override it and use `{{ block.super }}` instead of carving out a separate block here.

@author:
I'll refactor the view, I was trying to avoid using block.super but I guess its better that way

diff_hunk: @@ -112,6 +114,28 @@ def prep_table_data(self, request, queryset, parent):
         """
         return queryset
 
+    def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
+
+        return_url = '?return_url=' + request.get_full_path()
+        bulk_edit_url = reverse(f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_edit') + return_url
+        bulk_delete_url = reverse(f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_delete') + return_url
+
+        try:
+            bulk_rename_url = reverse(
+                f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_rename') + return_url
+        except NoReverseMatch:
+            bulk_rename_url = None
+
+        context.update({
+            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
+            'table_config': f'{self.table.__name__}_config',
+            'bulk_edit_url': bulk_edit_url,
+            'bulk_delete_url': bulk_delete_url,
+            'bulk_rename_url': bulk_rename_url,
path: netbox/netbox/views/generic/object_views.py
text: @user1:
We should be able to omit these from the template context, and just resolve them directly in the template using the `validated_viewname()` function from `utilities.templatetags.helpers`. I'll add an example in the template to clarify.

diff_hunk: @@ -90,6 +91,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
     child_model = None
     table = None
     filterset = None
+    template_name = 'inc/tab_view.html'
path: netbox/netbox/views/generic/object_views.py
text: @user1:
I'm a little hesitant about setting this on the view, because we currently support `ObjectChildrenView` for plugins. Convention is for subclasses to set their own `template_name` value of course, however the current behavior is to use `$app/$model.html` if it's left undefined.

To avoid a breaking change in v3.5, we could instead temporarily set `template_name = 'inc/tab_view.html'` under each of our subclasses, and defer the change in default behavior until v3.6.

@author:
Yes, the idea of the issue raised was to avoid writing such code to help reduce the boilerplate code. I think it would be best to remove this need in v3.6

diff_hunk: @@ -112,6 +114,28 @@ def prep_table_data(self, request, queryset, parent):
         """
         return queryset
 
+    def get_extra_context(self, request, instance):
+        context = super().get_extra_context(request, instance)
+
+        return_url = '?return_url=' + request.get_full_path()
+        bulk_edit_url = reverse(f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_edit') + return_url
+        bulk_delete_url = reverse(f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_delete') + return_url
+
+        try:
+            bulk_rename_url = reverse(
+                f'{self.child_model._meta.app_label}:{self.child_model._meta.model_name}_bulk_rename') + return_url
+        except NoReverseMatch:
+            bulk_rename_url = None
+
+        context.update({
+            'base_template': f'{instance._meta.app_label}/{instance._meta.model_name}.html',
path: netbox/netbox/views/generic/object_views.py
text: @user1:
We can move `base_template` to the context defined under `get()`.

diff_hunk: @@ -0,0 +1,55 @@
+{% extends base_template %}
+{% load helpers %}
+
+{% block content %}
+    {% include 'inc/table_controls_htmx.html' with table_modal=table_config %}
path: netbox/templates/inc/tab_view.html
text: @user1:
This isn't in scope for this FR, but more of a note to self: We should introduce a template tag for this to obviate the need to pass the `table_config` variable.

@user1:
I've opened #13400 to add a `name` property on BaseTable, which should be sufficient.

@user2:
should this not be `tab_view.html` but `objectchildren_view.html` perhaps instead?

@author:
This file has been moved to the `generic/object_tab.html` in the latest changes. I suggest you review from the last commit pushed