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

Enable HTMX for all ObjectChildrenViews

jeremystretch 4 лет назад
Родитель
Сommit
4ffa823ab8

+ 17 - 0
netbox/dcim/views.py

@@ -797,41 +797,49 @@ class DeviceTypeView(generic.ObjectView):
 class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
     child_model = ConsolePortTemplate
     table = tables.ConsolePortTemplateTable
+    filterset = filtersets.ConsolePortTemplateFilterSet
 
 
 class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
     child_model = ConsoleServerPortTemplate
     table = tables.ConsoleServerPortTemplateTable
+    filterset = filtersets.ConsoleServerPortTemplateFilterSet
 
 
 class DeviceTypePowerPortsView(DeviceTypeComponentsView):
     child_model = PowerPortTemplate
     table = tables.PowerPortTemplateTable
+    filterset = filtersets.PowerPortTemplateFilterSet
 
 
 class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
     child_model = PowerOutletTemplate
     table = tables.PowerOutletTemplateTable
+    filterset = filtersets.PowerOutletTemplateFilterSet
 
 
 class DeviceTypeInterfacesView(DeviceTypeComponentsView):
     child_model = InterfaceTemplate
     table = tables.InterfaceTemplateTable
+    filterset = filtersets.InterfaceTemplateFilterSet
 
 
 class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
     child_model = FrontPortTemplate
     table = tables.FrontPortTemplateTable
+    filterset = filtersets.FrontPortTemplateFilterSet
 
 
 class DeviceTypeRearPortsView(DeviceTypeComponentsView):
     child_model = RearPortTemplate
     table = tables.RearPortTemplateTable
+    filterset = filtersets.RearPortTemplateFilterSet
 
 
 class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
     child_model = DeviceBayTemplate
     table = tables.DeviceBayTemplateTable
+    filterset = filtersets.DeviceBayTemplateFilterSet
 
 
 class DeviceTypeEditView(generic.ObjectEditView):
@@ -1328,30 +1336,35 @@ class DeviceView(generic.ObjectView):
 class DeviceConsolePortsView(DeviceComponentsView):
     child_model = ConsolePort
     table = tables.DeviceConsolePortTable
+    filterset = filtersets.ConsolePortFilterSet
     template_name = 'dcim/device/consoleports.html'
 
 
 class DeviceConsoleServerPortsView(DeviceComponentsView):
     child_model = ConsoleServerPort
     table = tables.DeviceConsoleServerPortTable
+    filterset = filtersets.ConsoleServerPortFilterSet
     template_name = 'dcim/device/consoleserverports.html'
 
 
 class DevicePowerPortsView(DeviceComponentsView):
     child_model = PowerPort
     table = tables.DevicePowerPortTable
+    filterset = filtersets.PowerPortFilterSet
     template_name = 'dcim/device/powerports.html'
 
 
 class DevicePowerOutletsView(DeviceComponentsView):
     child_model = PowerOutlet
     table = tables.DevicePowerOutletTable
+    filterset = filtersets.PowerOutletFilterSet
     template_name = 'dcim/device/poweroutlets.html'
 
 
 class DeviceInterfacesView(DeviceComponentsView):
     child_model = Interface
     table = tables.DeviceInterfaceTable
+    filterset = filtersets.InterfaceFilterSet
     template_name = 'dcim/device/interfaces.html'
 
     def get_children(self, request, parent):
@@ -1364,24 +1377,28 @@ class DeviceInterfacesView(DeviceComponentsView):
 class DeviceFrontPortsView(DeviceComponentsView):
     child_model = FrontPort
     table = tables.DeviceFrontPortTable
+    filterset = filtersets.FrontPortFilterSet
     template_name = 'dcim/device/frontports.html'
 
 
 class DeviceRearPortsView(DeviceComponentsView):
     child_model = RearPort
     table = tables.DeviceRearPortTable
+    filterset = filtersets.RearPortFilterSet
     template_name = 'dcim/device/rearports.html'
 
 
 class DeviceDeviceBaysView(DeviceComponentsView):
     child_model = DeviceBay
     table = tables.DeviceDeviceBayTable
+    filterset = filtersets.DeviceBayFilterSet
     template_name = 'dcim/device/devicebays.html'
 
 
 class DeviceInventoryView(DeviceComponentsView):
     child_model = InventoryItem
     table = tables.DeviceInventoryItemTable
+    filterset = filtersets.InventoryItemFilterSet
     template_name = 'dcim/device/inventory.html'
 
 

+ 9 - 2
netbox/ipam/views.py

@@ -1,21 +1,22 @@
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Prefetch
 from django.db.models.expressions import RawSQL
-from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 
+from dcim.filtersets import InterfaceFilterSet
 from dcim.models import Device, Interface, Site
 from dcim.tables import SiteTable
 from netbox.views import generic
 from utilities.tables import paginate_table
 from utilities.utils import count_related
+from virtualization.filtersets import VMInterfaceFilterSet
 from virtualization.models import VirtualMachine, VMInterface
 from . import filtersets, forms, tables
 from .constants import *
 from .models import *
 from .models import ASN
-from .utils import add_available_ipaddresses, add_requested_prefixes, add_available_vlans
+from .utils import add_requested_prefixes, add_available_vlans
 
 
 #
@@ -457,6 +458,7 @@ class PrefixPrefixesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
     child_model = Prefix
     table = tables.PrefixTable
+    filterset = filtersets.PrefixFilterSet
     template_name = 'ipam/prefix/prefixes.html'
 
     def get_children(self, request, parent):
@@ -483,6 +485,7 @@ class PrefixIPRangesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
     child_model = IPRange
     table = tables.IPRangeTable
+    filterset = filtersets.IPRangeFilterSet
     template_name = 'ipam/prefix/ip_ranges.html'
 
     def get_children(self, request, parent):
@@ -499,6 +502,7 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
     child_model = IPAddress
     table = tables.IPAddressTable
+    filterset = filtersets.IPAddressFilterSet
     template_name = 'ipam/prefix/ip_addresses.html'
 
     def get_children(self, request, parent):
@@ -560,6 +564,7 @@ class IPRangeIPAddressesView(generic.ObjectChildrenView):
     queryset = IPRange.objects.all()
     child_model = IPAddress
     table = tables.IPAddressTable
+    filterset = filtersets.IPAddressFilterSet
     template_name = 'ipam/iprange/ip_addresses.html'
 
     def get_children(self, request, parent):
@@ -959,6 +964,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
     queryset = VLAN.objects.all()
     child_model = Interface
     table = tables.VLANDevicesTable
+    filterset = InterfaceFilterSet
     template_name = 'ipam/vlan/interfaces.html'
 
     def get_children(self, request, parent):
@@ -974,6 +980,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
     queryset = VLAN.objects.all()
     child_model = VMInterface
     table = tables.VLANVirtualMachinesTable
+    filterset = VMInterfaceFilterSet
     template_name = 'ipam/vlan/vminterfaces.html'
 
     def get_children(self, request, parent):

+ 12 - 1
netbox/netbox/views/generic.py

@@ -84,11 +84,12 @@ class ObjectChildrenView(ObjectView):
     queryset = None
     child_model = None
     table = None
+    filterset = None
     template_name = None
 
     def get_children(self, request, parent):
         """
-        Return a QuerySet or iterable of child objects.
+        Return a QuerySet of child objects.
 
         request: The current request
         parent: The parent object
@@ -102,6 +103,9 @@ class ObjectChildrenView(ObjectView):
         instance = get_object_or_404(self.queryset, **kwargs)
         child_objects = self.get_children(request, instance)
 
+        if self.filterset:
+            child_objects = self.filterset(request.GET, child_objects).qs
+
         permissions = {}
         for action in ('change', 'delete'):
             perm_name = get_permission_for_model(self.child_model, action)
@@ -113,6 +117,13 @@ class ObjectChildrenView(ObjectView):
             table.columns.show('pk')
         paginate_table(table, request)
 
+        # If this is an HTMX request, return only the rendered table HTML
+        if is_htmx(request):
+            return render(request, 'htmx/table.html', {
+                'object': instance,
+                'table': table,
+            })
+
         return render(request, self.get_template_name(), {
             'object': instance,
             'table': table,

+ 7 - 4
netbox/templates/dcim/device/consoleports.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsolePortTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_consoleport %}
@@ -38,6 +42,5 @@
         {% endif %}
     </div>
   </form>
-  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
   {% table_config_form table %}
 {% endblock %}

+ 7 - 3
netbox/templates/dcim/device/consoleserverports.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceConsoleServerPortTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_consoleserverport %}

+ 7 - 3
netbox/templates/dcim/device/devicebays.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceDeviceBayTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_devicebay %}

+ 7 - 3
netbox/templates/dcim/device/frontports.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceFrontPortTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_frontport %}

+ 15 - 3
netbox/templates/dcim/device/interfaces.html

@@ -9,7 +9,15 @@
     <div class="row mb-3 justify-content-between">
       <div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
         <div class="input-group input-group-sm">
-          <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
+          <input
+              type="text"
+              name="q"
+              class="form-control"
+              placeholder="Quick search"
+              hx-get="{{ request.full_path }}"
+              hx-target="#object_list"
+              hx-trigger="keyup changed delay:500ms"
+          />
         </div>
       </div>
       <div class="col col-md-3 mb-0 d-flex noprint table-controls">
@@ -34,9 +42,13 @@
         </div>
       </div>
     </div>
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
         {% if perms.dcim.change_interface %}

+ 7 - 3
netbox/templates/dcim/device/inventory.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceInventoryItemTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_inventoryitem %}

+ 7 - 3
netbox/templates/dcim/device/poweroutlets.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerOutletTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_powerport %}

+ 7 - 3
netbox/templates/dcim/device/powerports.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DevicePowerPortTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DevicePowerPortTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_powerport %}

+ 7 - 3
netbox/templates/dcim/device/rearports.html

@@ -6,10 +6,14 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="DeviceRearPortTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceRearPortTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint bulk-buttons">
         <div class="bulk-button-group">
             {% if perms.dcim.change_rearport %}

+ 7 - 11
netbox/templates/dcim/devicetype/component_templates.html

@@ -7,11 +7,9 @@
     <form method="post">
         {% csrf_token %}
         <div class="card">
-            <h5 class="card-header">
-                {{ title }}
-            </h5>
-            <div class="card-body table-responsive">
-                {% render_table table 'inc/table.html' %}
+            <h5 class="card-header">{{ title }}</h5>
+            <div class="card-body" id="object_list">
+              {% include 'htmx/table.html' %}
             </div>
             <div class="card-footer noprint">
                 {% if table.rows %}
@@ -37,12 +35,10 @@
     </form>
   {% else %}
     <div class="card">
-        <h5 class="card-header">
-            {{ title }}
-        </h5>
-        <div class="card-body table-responsive">
-            {% render_table table 'inc/table.html' %}
-        </div>
+      <h5 class="card-header">{{ title }}</h5>
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
   {% endif %}
 {% endblock content %}

+ 26 - 4
netbox/templates/ipam/iprange/ip_addresses.html

@@ -1,4 +1,5 @@
 {% extends 'ipam/iprange/base.html' %}
+{% load helpers %}
 
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
@@ -9,9 +10,30 @@
 {% endblock %}
 
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_ipaddress %}
+          <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 perms.ipam.delete_ipaddress %}
+          <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>
-  </div>
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 25 - 7
netbox/templates/ipam/prefix/ip_addresses.html

@@ -1,6 +1,5 @@
 {% extends 'ipam/prefix/base.html' %}
 {% load helpers %}
-{% load static %}
 
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and first_available_ip %}
@@ -11,11 +10,30 @@
 {% endblock %}
 
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'inc/table_controls.html' with table_modal="IPAddressTable_config" %}
-      {% include 'utilities/obj_table.html' with heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_ipaddress %}
+          <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 perms.ipam.delete_ipaddress %}
+          <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>
-  </div>
-  {% table_config_form table table_name="IPAddressTable" %}
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 25 - 7
netbox/templates/ipam/prefix/ip_ranges.html

@@ -1,13 +1,31 @@
 {% extends 'ipam/prefix/base.html' %}
 {% load helpers %}
-{% load static %}
 
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'inc/table_controls.html' with table_modal="IPRangeTable_config" %}
-      {% include 'utilities/obj_table.html' with heading='Child IP Ranges' bulk_edit_url='ipam:iprange_bulk_edit' bulk_delete_url='ipam:iprange_bulk_delete' parent=prefix %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_iprange %}
+          <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 perms.ipam.delete_iprange %}
+          <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>
-  </div>
-  {% table_config_form table table_name="IPRangeTable" %}
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 25 - 7
netbox/templates/ipam/prefix/prefixes.html

@@ -1,6 +1,5 @@
 {% extends 'ipam/prefix/base.html' %}
 {% load helpers %}
-{% load static %}
 
 {% block extra_controls %}
   {% include 'ipam/inc/toggle_available.html' %}
@@ -13,11 +12,30 @@
 {% endblock %}
 
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'inc/table_controls.html' with table_modal="PrefixTable_config" %}
-      {% include 'utilities/obj_table.html' with heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_prefix %}
+          <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 perms.ipam.delete_prefix %}
+          <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>
-  </div>
-  {% table_config_form table table_name="PrefixTable" %}
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 12 - 4
netbox/templates/ipam/vlan/interfaces.html

@@ -1,9 +1,17 @@
 {% extends 'ipam/vlan/base.html' %}
+{% load helpers %}
 
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with heading='Device Interfaces' parent=vlan %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="VLANDevicesTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
-  </div>
+
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 12 - 4
netbox/templates/ipam/vlan/vminterfaces.html

@@ -1,9 +1,17 @@
 {% extends 'ipam/vlan/base.html' %}
+{% load helpers %}
 
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with heading='Virtual Machine Interfaces' parent=vlan %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="VLANVirtualMachinesTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
-  </div>
+
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 15 - 16
netbox/templates/virtualization/cluster/devices.html

@@ -3,26 +3,25 @@
 {% load render_table from django_tables2 %}
 
 {% block content %}
-<div class="row">
-  <div class="col col-md-12">
+  <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
+
     <div class="card">
-      <h5 class="card-header">
-        Host Devices
-      </h5>
-      <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
-      {% csrf_token %}
-      <div class="card-body table-responsive">
-        {% render_table table 'inc/table.html' %}
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
       </div>
-      {% if perms.virtualization.change_cluster %}
-        <div class="card-footer noprint">
+    </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>
-        </div>
-      {% endif %}
-      </form>
+        {% endif %}
+      </div>
     </div>
-  </div>
-</div>
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 23 - 9
netbox/templates/virtualization/cluster/virtual_machines.html

@@ -3,16 +3,30 @@
 {% load render_table from django_tables2 %}
 
 {% block content %}
-<div class="row">
-  <div class="col col-md-12">
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
+
     <div class="card">
-      <h5 class="card-header">
-        Virtual Machines
-      </h5>
-      <div class="card-body table-responsive">
-        {% render_table table 'inc/table.html' %}
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.virtualization.change_virtualmachine %}
+          <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 perms.virtualization.delete_virtualmachine %}
+          <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>
-  </div>
-</div>
+  </form>
+  {% table_config_form table %}
 {% endblock %}

+ 7 - 4
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -1,15 +1,18 @@
 {% extends 'virtualization/virtualmachine/base.html' %}
 {% load render_table from django_tables2 %}
 {% load helpers %}
-{% load static %}
 
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    {% include 'inc/table_controls.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="VMInterfaceTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
     </div>
+
     <div class="noprint">
         {% if perms.virtualization.change_vminterface %}
             <button type="submit" name="_rename" formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-warning btn-sm">

+ 4 - 0
netbox/virtualization/views.py

@@ -4,6 +4,7 @@ from django.db.models import Prefetch
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 
+from dcim.filtersets import DeviceFilterSet
 from dcim.models import Device
 from dcim.tables import DeviceTable
 from extras.views import ObjectConfigContextView
@@ -165,6 +166,7 @@ class ClusterVirtualMachinesView(generic.ObjectChildrenView):
     queryset = Cluster.objects.all()
     child_model = VirtualMachine
     table = tables.VirtualMachineTable
+    filterset = filtersets.VirtualMachineFilterSet
     template_name = 'virtualization/cluster/virtual_machines.html'
 
     def get_children(self, request, parent):
@@ -180,6 +182,7 @@ class ClusterDevicesView(generic.ObjectChildrenView):
     queryset = Cluster.objects.all()
     child_model = Device
     table = DeviceTable
+    filterset = DeviceFilterSet
     template_name = 'virtualization/cluster/devices.html'
 
     def get_children(self, request, parent):
@@ -345,6 +348,7 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
     queryset = VirtualMachine.objects.all()
     child_model = VMInterface
     table = tables.VMInterfaceTable
+    filterset = filtersets.VMInterfaceFilterSet
     template_name = 'virtualization/virtualmachine/interfaces.html'
 
     def get_children(self, request, parent):