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

Closes #8184: Enable HTMX for embedded tables (#11518)

* Enable HTMX rendering for embedded tables

* Start converting embedded tables to use HTMX (WIP)

* Additional table conversions (WIP)

* Standardize HTMX usage for nested group models

* Enable HTMX for additional emebedded tables

* Fix HTMX table rendering for ObjectChildrenView

* Standardize usage of inc/panel_table.html

* Hide selection boxes in embedded tables
Jeremy Stretch 3 лет назад
Родитель
Сommit
1a2dae3471
65 измененных файлов с 381 добавлено и 667 удалено
  1. 0 38
      netbox/circuits/views.py
  2. 9 136
      netbox/dcim/views.py
  3. 2 58
      netbox/ipam/views.py
  4. 16 1
      netbox/netbox/tables/tables.py
  5. 6 1
      netbox/netbox/views/generic/bulk_views.py
  6. 5 5
      netbox/templates/circuits/circuittype.html
  7. 5 5
      netbox/templates/circuits/provider.html
  8. 4 4
      netbox/templates/circuits/providernetwork.html
  9. 1 1
      netbox/templates/dcim/connections_list.html
  10. 1 1
      netbox/templates/dcim/device/consoleports.html
  11. 1 1
      netbox/templates/dcim/device/consoleserverports.html
  12. 1 1
      netbox/templates/dcim/device/devicebays.html
  13. 1 1
      netbox/templates/dcim/device/frontports.html
  14. 1 1
      netbox/templates/dcim/device/interfaces.html
  15. 1 1
      netbox/templates/dcim/device/inventory.html
  16. 1 1
      netbox/templates/dcim/device/modulebays.html
  17. 1 1
      netbox/templates/dcim/device/poweroutlets.html
  18. 1 1
      netbox/templates/dcim/device/powerports.html
  19. 1 1
      netbox/templates/dcim/device/rearports.html
  20. 2 2
      netbox/templates/dcim/devicerole.html
  21. 2 2
      netbox/templates/dcim/devicetype/component_templates.html
  22. 4 7
      netbox/templates/dcim/interface.html
  23. 5 5
      netbox/templates/dcim/location.html
  24. 7 7
      netbox/templates/dcim/manufacturer.html
  25. 2 2
      netbox/templates/dcim/moduletype/component_templates.html
  26. 5 5
      netbox/templates/dcim/platform.html
  27. 36 34
      netbox/templates/dcim/powerpanel.html
  28. 5 5
      netbox/templates/dcim/rackrole.html
  29. 11 14
      netbox/templates/dcim/region.html
  30. 11 14
      netbox/templates/dcim/sitegroup.html
  31. 10 4
      netbox/templates/dcim/virtualdevicecontext.html
  32. 1 1
      netbox/templates/generic/object_list.html
  33. 4 3
      netbox/templates/home.html
  34. 2 2
      netbox/templates/htmx/table.html
  35. 12 12
      netbox/templates/inc/paginator_htmx.html
  36. 10 12
      netbox/templates/inc/panel_table.html
  37. 53 56
      netbox/templates/inc/table_htmx.html
  38. 1 1
      netbox/templates/ipam/aggregate/prefixes.html
  39. 8 8
      netbox/templates/ipam/asn.html
  40. 5 17
      netbox/templates/ipam/fhrpgroup.html
  41. 2 23
      netbox/templates/ipam/ipaddress.html
  42. 1 1
      netbox/templates/ipam/iprange/ip_addresses.html
  43. 4 3
      netbox/templates/ipam/l2vpn.html
  44. 1 1
      netbox/templates/ipam/prefix/ip_addresses.html
  45. 1 1
      netbox/templates/ipam/prefix/ip_ranges.html
  46. 1 1
      netbox/templates/ipam/prefix/prefixes.html
  47. 6 6
      netbox/templates/ipam/rir.html
  48. 8 20
      netbox/templates/ipam/role.html
  49. 15 17
      netbox/templates/ipam/vlan.html
  50. 1 1
      netbox/templates/ipam/vlan/interfaces.html
  51. 1 1
      netbox/templates/ipam/vlan/vminterfaces.html
  52. 1 1
      netbox/templates/search.html
  53. 14 17
      netbox/templates/tenancy/contactgroup.html
  54. 19 5
      netbox/templates/tenancy/tenantgroup.html
  55. 1 1
      netbox/templates/virtualization/cluster/devices.html
  56. 1 1
      netbox/templates/virtualization/cluster/virtual_machines.html
  57. 5 5
      netbox/templates/virtualization/clustergroup.html
  58. 5 5
      netbox/templates/virtualization/clustertype.html
  59. 1 1
      netbox/templates/virtualization/virtualmachine/interfaces.html
  60. 5 10
      netbox/templates/virtualization/vminterface.html
  61. 20 6
      netbox/templates/wireless/wirelesslangroup.html
  62. 0 37
      netbox/tenancy/views.py
  63. 14 0
      netbox/utilities/htmx.py
  64. 0 20
      netbox/virtualization/views.py
  65. 0 11
      netbox/wireless/views.py

+ 0 - 38
netbox/circuits/views.py

@@ -29,20 +29,6 @@ class ProviderListView(generic.ObjectListView):
 class ProviderView(generic.ObjectView):
     queryset = Provider.objects.all()
 
-    def get_extra_context(self, request, instance):
-        circuits = Circuit.objects.restrict(request.user, 'view').filter(
-            provider=instance
-        ).prefetch_related(
-            'tenant__group', 'termination_a__site', 'termination_z__site',
-            'termination_a__provider_network', 'termination_z__provider_network',
-        )
-        circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('provider',))
-        circuits_table.configure(request)
-
-        return {
-            'circuits_table': circuits_table,
-        }
-
 
 @register_model_view(Provider, 'edit')
 class ProviderEditView(generic.ObjectEditView):
@@ -93,21 +79,6 @@ class ProviderNetworkListView(generic.ObjectListView):
 class ProviderNetworkView(generic.ObjectView):
     queryset = ProviderNetwork.objects.all()
 
-    def get_extra_context(self, request, instance):
-        circuits = Circuit.objects.restrict(request.user, 'view').filter(
-            Q(termination_a__provider_network=instance.pk) |
-            Q(termination_z__provider_network=instance.pk)
-        ).prefetch_related(
-            'tenant__group', 'termination_a__site', 'termination_z__site',
-            'termination_a__provider_network', 'termination_z__provider_network',
-        )
-        circuits_table = tables.CircuitTable(circuits, user=request.user)
-        circuits_table.configure(request)
-
-        return {
-            'circuits_table': circuits_table,
-        }
-
 
 @register_model_view(ProviderNetwork, 'edit')
 class ProviderNetworkEditView(generic.ObjectEditView):
@@ -156,15 +127,6 @@ class CircuitTypeListView(generic.ObjectListView):
 class CircuitTypeView(generic.ObjectView):
     queryset = CircuitType.objects.all()
 
-    def get_extra_context(self, request, instance):
-        circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance)
-        circuits_table = tables.CircuitTable(circuits, user=request.user, exclude=('type',))
-        circuits_table.configure(request)
-
-        return {
-            'circuits_table': circuits_table,
-        }
-
 
 @register_model_view(CircuitType, 'edit')
 class CircuitTypeEditView(generic.ObjectEditView):

+ 9 - 136
netbox/dcim/views.py

@@ -14,7 +14,7 @@ from django.views.generic import View
 from circuits.models import Circuit, CircuitTermination
 from extras.views import ObjectConfigContextView
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
-from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
+from ipam.tables import InterfaceVLANTable
 from netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -212,30 +212,6 @@ class RegionListView(generic.ObjectListView):
 class RegionView(generic.ObjectView):
     queryset = Region.objects.all()
 
-    def get_extra_context(self, request, instance):
-        child_regions = Region.objects.add_related_count(
-            Region.objects.all(),
-            Site,
-            'region',
-            'site_count',
-            cumulative=True
-        ).restrict(request.user, 'view').filter(
-            parent__in=instance.get_descendants(include_self=True)
-        )
-        child_regions_table = tables.RegionTable(child_regions)
-        child_regions_table.columns.hide('actions')
-
-        sites = Site.objects.restrict(request.user, 'view').filter(
-            region=instance
-        )
-        sites_table = tables.SiteTable(sites, user=request.user, exclude=('region',))
-        sites_table.configure(request)
-
-        return {
-            'child_regions_table': child_regions_table,
-            'sites_table': sites_table,
-        }
-
 
 @register_model_view(Region, 'edit')
 class RegionEditView(generic.ObjectEditView):
@@ -300,30 +276,6 @@ class SiteGroupListView(generic.ObjectListView):
 class SiteGroupView(generic.ObjectView):
     queryset = SiteGroup.objects.all()
 
-    def get_extra_context(self, request, instance):
-        child_groups = SiteGroup.objects.add_related_count(
-            SiteGroup.objects.all(),
-            Site,
-            'group',
-            'site_count',
-            cumulative=True
-        ).restrict(request.user, 'view').filter(
-            parent__in=instance.get_descendants(include_self=True)
-        )
-        child_groups_table = tables.SiteGroupTable(child_groups)
-        child_groups_table.columns.hide('actions')
-
-        sites = Site.objects.restrict(request.user, 'view').filter(
-            group=instance
-        )
-        sites_table = tables.SiteTable(sites, user=request.user, exclude=('group',))
-        sites_table.configure(request)
-
-        return {
-            'child_groups_table': child_groups_table,
-            'sites_table': sites_table,
-        }
-
 
 @register_model_view(SiteGroup, 'edit')
 class SiteGroupEditView(generic.ObjectEditView):
@@ -493,22 +445,6 @@ class LocationView(generic.ObjectView):
         rack_count = Rack.objects.filter(location__in=location_ids).count()
         device_count = Device.objects.filter(location__in=location_ids).count()
 
-        child_locations = Location.objects.add_related_count(
-            Location.objects.add_related_count(
-                Location.objects.all(),
-                Device,
-                'location',
-                'device_count',
-                cumulative=True
-            ),
-            Rack,
-            'location',
-            'rack_count',
-            cumulative=True
-        ).filter(pk__in=location_ids).exclude(pk=instance.pk)
-        child_locations_table = tables.LocationTable(child_locations, user=request.user)
-        child_locations_table.configure(request)
-
         nonracked_devices = Device.objects.filter(
             location=instance,
             rack__isnull=True,
@@ -518,7 +454,6 @@ class LocationView(generic.ObjectView):
         return {
             'rack_count': rack_count,
             'device_count': device_count,
-            'child_locations_table': child_locations_table,
             'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
             'total_nonracked_devices_count': nonracked_devices.count(),
         }
@@ -583,20 +518,6 @@ class RackRoleListView(generic.ObjectListView):
 class RackRoleView(generic.ObjectView):
     queryset = RackRole.objects.all()
 
-    def get_extra_context(self, request, instance):
-        racks = Rack.objects.restrict(request.user, 'view').filter(role=instance).annotate(
-            device_count=count_related(Device, 'rack')
-        )
-
-        racks_table = tables.RackTable(racks, user=request.user, exclude=(
-            'role', 'get_utilization', 'get_power_utilization',
-        ))
-        racks_table.configure(request)
-
-        return {
-            'racks_table': racks_table,
-        }
-
 
 @register_model_view(RackRole, 'edit')
 class RackRoleEditView(generic.ObjectEditView):
@@ -859,8 +780,6 @@ class ManufacturerView(generic.ObjectView):
     def get_extra_context(self, request, instance):
         device_types = DeviceType.objects.restrict(request.user, 'view').filter(
             manufacturer=instance
-        ).annotate(
-            instance_count=count_related(Device, 'device_type')
         )
         module_types = ModuleType.objects.restrict(request.user, 'view').filter(
             manufacturer=instance
@@ -869,13 +788,10 @@ class ManufacturerView(generic.ObjectView):
             manufacturer=instance
         )
 
-        devicetypes_table = tables.DeviceTypeTable(device_types, user=request.user, exclude=('manufacturer',))
-        devicetypes_table.configure(request)
-
         return {
-            'devicetypes_table': devicetypes_table,
-            'inventory_item_count': inventory_items.count(),
-            'module_type_count': module_types.count(),
+            'devicetype_count': device_types.count(),
+            'inventoryitem_count': inventory_items.count(),
+            'moduletype_count': module_types.count(),
         }
 
 
@@ -1726,19 +1642,6 @@ class DeviceRoleListView(generic.ObjectListView):
 class DeviceRoleView(generic.ObjectView):
     queryset = DeviceRole.objects.all()
 
-    def get_extra_context(self, request, instance):
-        devices = Device.objects.restrict(request.user, 'view').filter(
-            device_role=instance
-        )
-        devices_table = tables.DeviceTable(devices, user=request.user, exclude=('device_role',))
-        devices_table.configure(request)
-
-        return {
-            'devices_table': devices_table,
-            'device_count': Device.objects.filter(device_role=instance).count(),
-            'virtualmachine_count': VirtualMachine.objects.filter(role=instance).count(),
-        }
-
 
 @register_model_view(DeviceRole, 'devices', path='devices')
 class DeviceRoleDevicesView(generic.ObjectChildrenView):
@@ -1833,12 +1736,13 @@ class PlatformView(generic.ObjectView):
         devices = Device.objects.restrict(request.user, 'view').filter(
             platform=instance
         )
-        devices_table = tables.DeviceTable(devices, user=request.user, exclude=('platform',))
-        devices_table.configure(request)
+        virtual_machines = VirtualMachine.objects.restrict(request.user, 'view').filter(
+            platform=instance
+        )
 
         return {
-            'devices_table': devices_table,
-            'virtualmachine_count': VirtualMachine.objects.filter(platform=instance).count()
+            'device_count': devices.count(),
+            'virtualmachine_count': virtual_machines.count()
         }
 
 
@@ -2520,12 +2424,6 @@ class InterfaceView(generic.ObjectView):
             orderable=False
         )
 
-        # Get assigned IP addresses
-        ipaddress_table = AssignedIPAddressesTable(
-            data=instance.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant'),
-            orderable=False
-        )
-
         # Get bridge interfaces
         bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
         bridge_interfaces_tables = tables.InterfaceTable(
@@ -2558,7 +2456,6 @@ class InterfaceView(generic.ObjectView):
 
         return {
             'vdc_table': vdc_table,
-            'ipaddress_table': ipaddress_table,
             'bridge_interfaces_table': bridge_interfaces_tables,
             'child_interfaces_table': child_interfaces_tables,
             'vlan_table': vlan_table,
@@ -3533,20 +3430,6 @@ class PowerPanelListView(generic.ObjectListView):
 class PowerPanelView(generic.ObjectView):
     queryset = PowerPanel.objects.all()
 
-    def get_extra_context(self, request, instance):
-        power_feeds = PowerFeed.objects.restrict(request.user).filter(power_panel=instance)
-        powerfeed_table = tables.PowerFeedTable(
-            data=power_feeds,
-            orderable=False
-        )
-        if request.user.has_perm('dcim.delete_cable'):
-            powerfeed_table.columns.show('pk')
-        powerfeed_table.exclude = ['power_panel']
-
-        return {
-            'powerfeed_table': powerfeed_table,
-        }
-
 
 @register_model_view(PowerPanel, 'edit')
 class PowerPanelEditView(generic.ObjectEditView):
@@ -3648,16 +3531,6 @@ class VirtualDeviceContextListView(generic.ObjectListView):
 class VirtualDeviceContextView(generic.ObjectView):
     queryset = VirtualDeviceContext.objects.all()
 
-    def get_extra_context(self, request, instance):
-        interfaces_table = tables.InterfaceTable(instance.interfaces, user=request.user)
-        interfaces_table.configure(request)
-        interfaces_table.columns.hide('device')
-
-        return {
-            'interfaces_table': interfaces_table,
-            'interface_count': instance.interfaces.count(),
-        }
-
 
 @register_model_view(VirtualDeviceContext, 'edit')
 class VirtualDeviceContextEditView(generic.ObjectEditView):

+ 2 - 58
netbox/ipam/views.py

@@ -5,11 +5,9 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.translation import gettext as _
 
-from circuits.models import Provider, Circuit
-from circuits.tables import ProviderTable
+from circuits.models import Provider
 from dcim.filtersets import InterfaceFilterSet
 from dcim.models import Interface, Site, Device
-from dcim.tables import SiteTable
 from netbox.views import generic
 from utilities.utils import count_related
 from utilities.views import ViewTab, register_model_view
@@ -167,17 +165,6 @@ class RIRListView(generic.ObjectListView):
 class RIRView(generic.ObjectView):
     queryset = RIR.objects.all()
 
-    def get_extra_context(self, request, instance):
-        aggregates = Aggregate.objects.restrict(request.user, 'view').filter(rir=instance).annotate(
-            child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
-        )
-        aggregates_table = tables.AggregateTable(aggregates, user=request.user, exclude=('rir', 'utilization'))
-        aggregates_table.configure(request)
-
-        return {
-            'aggregates_table': aggregates_table,
-        }
-
 
 @register_model_view(RIR, 'edit')
 class RIREditView(generic.ObjectEditView):
@@ -232,22 +219,11 @@ class ASNView(generic.ObjectView):
     queryset = ASN.objects.all()
 
     def get_extra_context(self, request, instance):
-        # Gather assigned Sites
         sites = instance.sites.restrict(request.user, 'view')
-        sites_table = SiteTable(sites, user=request.user)
-        sites_table.configure(request)
-
-        # Gather assigned Providers
-        providers = instance.providers.restrict(request.user, 'view').annotate(
-            count_circuits=count_related(Circuit, 'provider')
-        )
-        providers_table = ProviderTable(providers, user=request.user)
-        providers_table.configure(request)
+        providers = instance.providers.restrict(request.user, 'view')
 
         return {
-            'sites_table': sites_table,
             'sites_count': sites.count(),
-            'providers_table': providers_table,
             'providers_count': providers.count(),
         }
 
@@ -392,18 +368,6 @@ class RoleListView(generic.ObjectListView):
 class RoleView(generic.ObjectView):
     queryset = Role.objects.all()
 
-    def get_extra_context(self, request, instance):
-        prefixes = Prefix.objects.restrict(request.user, 'view').filter(
-            role=instance
-        )
-
-        prefixes_table = tables.PrefixTable(prefixes, user=request.user, exclude=('role', 'utilization'))
-        prefixes_table.configure(request)
-
-        return {
-            'prefixes_table': prefixes_table,
-        }
-
 
 @register_model_view(Role, 'edit')
 class RoleEditView(generic.ObjectEditView):
@@ -750,7 +714,6 @@ class IPAddressView(generic.ObjectView):
         return {
             'parent_prefixes_table': parent_prefixes_table,
             'duplicate_ips_table': duplicate_ips_table,
-            'more_duplicate_ips': duplicate_ips.count() > 10,
             'related_ips_table': related_ips_table,
             'services': services,
         }
@@ -888,17 +851,9 @@ class VLANGroupView(generic.ObjectView):
             vlans_table.columns.show('pk')
         vlans_table.configure(request)
 
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'add': request.user.has_perm('ipam.add_vlan'),
-            'change': request.user.has_perm('ipam.change_vlan'),
-            'delete': request.user.has_perm('ipam.delete_vlan'),
-        }
-
         return {
             'vlans_count': vlans_count,
             'vlans_table': vlans_table,
-            'permissions': permissions,
         }
 
 
@@ -954,11 +909,6 @@ class FHRPGroupView(generic.ObjectView):
     queryset = FHRPGroup.objects.all()
 
     def get_extra_context(self, request, instance):
-        # Get assigned IP addresses
-        ipaddress_table = tables.AssignedIPAddressesTable(
-            data=instance.ip_addresses.restrict(request.user, 'view'),
-            orderable=False
-        )
 
         # Get assigned interfaces
         members_table = tables.FHRPGroupAssignmentTable(
@@ -968,7 +918,6 @@ class FHRPGroupView(generic.ObjectView):
         members_table.columns.hide('group')
 
         return {
-            'ipaddress_table': ipaddress_table,
             'members_table': members_table,
             'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
         }
@@ -1250,10 +1199,6 @@ class L2VPNView(generic.ObjectView):
     queryset = L2VPN.objects.all()
 
     def get_extra_context(self, request, instance):
-        terminations = L2VPNTermination.objects.restrict(request.user, 'view').filter(l2vpn=instance)
-        terminations_table = tables.L2VPNTerminationTable(terminations, user=request.user, exclude=('l2vpn', ))
-        terminations_table.configure(request)
-
         import_targets_table = tables.RouteTargetTable(
             instance.import_targets.prefetch_related('tenant'),
             orderable=False
@@ -1264,7 +1209,6 @@ class L2VPNView(generic.ObjectView):
         )
 
         return {
-            'terminations_table': terminations_table,
             'import_targets_table': import_targets_table,
             'export_targets_table': export_targets_table,
         }

+ 16 - 1
netbox/netbox/tables/tables.py

@@ -4,6 +4,8 @@ from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldDoesNotExist
 from django.db.models.fields.related import RelatedField
+from django.urls import reverse
+from django.urls.exceptions import NoReverseMatch
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 from django_tables2.data import TableQuerysetData
@@ -12,7 +14,7 @@ from extras.models import CustomField, CustomLink
 from extras.choices import CustomFieldVisibilityChoices
 from netbox.tables import columns
 from utilities.paginator import EnhancedPaginator, get_paginate_count
-from utilities.utils import highlight_string, title
+from utilities.utils import get_viewname, highlight_string, title
 
 __all__ = (
     'BaseTable',
@@ -197,6 +199,19 @@ class NetBoxTable(BaseTable):
 
         super().__init__(*args, extra_columns=extra_columns, **kwargs)
 
+    @property
+    def htmx_url(self):
+        """
+        Return the base HTML request URL for embedded tables.
+        """
+        if getattr(self, 'embedded', False):
+            viewname = get_viewname(self._meta.model, action='list')
+            try:
+                return reverse(viewname)
+            except NoReverseMatch:
+                pass
+        return ''
+
 
 class SearchTable(tables.Table):
     object_type = columns.ContentTypeColumn(

+ 6 - 1
netbox/netbox/views/generic/bulk_views.py

@@ -20,7 +20,7 @@ from utilities.choices import ImportFormatChoices
 from utilities.error_handlers import handle_protectederror
 from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
 from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
-from utilities.htmx import is_htmx
+from utilities.htmx import is_embedded, is_htmx
 from utilities.permissions import get_permission_for_model
 from utilities.views import GetReturnURLMixin
 from .base import BaseMultiObjectView
@@ -161,6 +161,11 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
 
         # If this is an HTMX request, return only the rendered table HTML
         if is_htmx(request):
+            if is_embedded(request):
+                table.embedded = True
+                # Hide selection checkboxes
+                if 'pk' in table.base_columns:
+                    table.columns.hide('pk')
             return render(request, 'htmx/table.html', {
                 'table': table,
             })

+ 5 - 5
netbox/templates/circuits/circuittype.html

@@ -31,7 +31,7 @@
           <tr>
             <th scope="row">Circuits</th>
             <td>
-              <a href="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}">{{ circuits_table.rows|length }}</a>
+              <a href="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}">{{ object.circuits.count }}</a>
             </td>
           </tr>
         </table>
@@ -49,10 +49,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Circuits</h5>
-      <div class="card-body table-responsive">
-        {% render_table circuits_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'circuits:circuit_list' %}?type_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 5 - 5
netbox/templates/circuits/provider.html

@@ -40,7 +40,7 @@
                     <tr>
                         <th scope="row">Circuits</th>
                         <td>
-                            <a href="{% url 'circuits:circuit_list' %}?provider={{ object.slug }}">{{ circuits_table.rows|length }}</a>
+                            <a href="{% url 'circuits:circuit_list' %}?provider={{ object.slug }}">{{ object.circuits.count }}</a>
                         </td>
                     </tr>
                 </table>
@@ -60,10 +60,10 @@
   <div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Circuits</h5>
-      <div class="card-body table-responsive">
-        {% render_table circuits_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'circuits:circuit_list' %}?provider_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 4 - 4
netbox/templates/circuits/providernetwork.html

@@ -50,10 +50,10 @@
   <div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Circuits</h5>
-      <div class="card-body table-responsive">
-        {% render_table circuits_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'circuits:circuit_list' %}?provider_network_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 1 - 1
netbox/templates/dcim/connections_list.html

@@ -12,7 +12,7 @@
     <div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
       {% include 'inc/table_controls_htmx.html' %}
       <div class="card">
-        <div class="card-body" id="object_list">
+        <div class="card-body htmx-container table-responsive" id="object_list">
           {% include 'htmx/table.html' %}
         </div>
       </div>

+ 1 - 1
netbox/templates/dcim/device/consoleports.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/consoleserverports.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/devicebays.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/frontports.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/interfaces.html

@@ -10,7 +10,7 @@
   {% csrf_token %}
 
   <div class="card">
-    <div class="card-body" id="object_list">
+    <div class="card-body htmx-container table-responsive" id="object_list">
       {% include 'htmx/table.html' %}
     </div>
   </div>

+ 1 - 1
netbox/templates/dcim/device/inventory.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/modulebays.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/poweroutlets.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/powerports.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/dcim/device/rearports.html

@@ -10,7 +10,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 2 - 2
netbox/templates/dcim/devicerole.html

@@ -45,14 +45,14 @@
           <tr>
             <th scope="row">Devices</th>
             <td>
-              <a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ device_count }}</a>
+              <a href="{% url 'dcim:device_list' %}?role_id={{ object.pk }}">{{ object.devices.count }}</a>
             </td>
           </tr>
           <tr>
             <th>Virtual Machines</th>
             <td>
               {% if object.vm_role %}
-                <a href="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}">{{ virtualmachine_count }}</a>
+                <a href="{% url 'virtualization:virtualmachine_list' %}?role_id={{ object.pk }}">{{ object.virtual_machines.count }}</a>
               {% else %}
                 {{ ''|placeholder }}
               {% endif %}

+ 2 - 2
netbox/templates/dcim/devicetype/component_templates.html

@@ -8,7 +8,7 @@
         {% csrf_token %}
         <div class="card">
             <h5 class="card-header">{{ title }}</h5>
-            <div class="card-body" id="object_list">
+            <div class="card-body htmx-container table-responsive" id="object_list">
               {% include 'htmx/table.html' %}
             </div>
             <div class="card-footer noprint">
@@ -36,7 +36,7 @@
   {% else %}
     <div class="card">
       <h5 class="card-header">{{ title }}</h5>
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 4 - 7
netbox/templates/dcim/interface.html

@@ -344,13 +344,10 @@
     <div class="col col-md-12">
       <div class="card">
         <h5 class="card-header">IP Addresses</h5>
-        <div class="card-body table-responsive">
-          {% if ipaddress_table.rows %}
-            {% render_table ipaddress_table 'inc/table.html' %}
-          {% else %}
-            <div class="text-muted">None</div>
-          {% endif %}
-        </div>
+        <div class="card-body htmx-container table-responsive"
+          hx-get="{% url 'ipam:ipaddress_list' %}?interface_id={{ object.pk }}"
+          hx-trigger="load"
+        ></div>
         {% if perms.ipam.add_ipaddress %}
           <div class="card-footer text-end noprint">
             <a href="{% url 'ipam:ipaddress_add' %}?device={{ object.device.pk }}&interface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">

+ 5 - 5
netbox/templates/dcim/location.html

@@ -92,11 +92,11 @@
 <div class="row mb-3">
 	<div class="col col-md-12">
     <div class="card">
-      <h5 class="card-header">Locations</h5>
-      <div class="card-body table-responsive">
-        {% render_table child_locations_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=child_locations_table.paginator page=child_locations_table.page %}
-      </div>
+      <h5 class="card-header">Child Locations</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:location_list' %}?parent_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 7 - 7
netbox/templates/dcim/manufacturer.html

@@ -45,19 +45,19 @@
           <tr>
             <th scope="row">Device types</th>
             <td>
-              <a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}">{{ devicetypes_table.rows|length }}</a>
+              <a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}">{{ devicetype_count }}</a>
             </td>
           </tr>
           <tr>
             <th scope="row">Module types</th>
             <td>
-              <a href="{% url 'dcim:moduletype_list' %}?manufacturer_id={{ object.pk }}">{{ module_type_count }}</a>
+              <a href="{% url 'dcim:moduletype_list' %}?manufacturer_id={{ object.pk }}">{{ moduletype_count }}</a>
             </td>
           </tr>
           <tr>
             <th scope="row">Inventory Items</th>
             <td>
-              <a href="{% url 'dcim:inventoryitem_list' %}?manufacturer_id={{ object.pk }}">{{ inventory_item_count }}</a>
+              <a href="{% url 'dcim:inventoryitem_list' %}?manufacturer_id={{ object.pk }}">{{ inventoryitem_count }}</a>
             </td>
           </tr>
         </table>
@@ -76,10 +76,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Device Types</h5>
-      <div class="card-body table-responsive">
-        {% render_table devicetypes_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=devicetypes_table.paginator page=devicetypes_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 2 - 2
netbox/templates/dcim/moduletype/component_templates.html

@@ -8,7 +8,7 @@
         {% csrf_token %}
         <div class="card">
             <h5 class="card-header">{{ title }}</h5>
-            <div class="card-body" id="object_list">
+            <div class="card-body htmx-container table-responsive" id="object_list">
               {% include 'htmx/table.html' %}
             </div>
             <div class="card-footer noprint">
@@ -36,7 +36,7 @@
   {% else %}
     <div class="card">
       <h5 class="card-header">{{ title }}</h5>
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 5 - 5
netbox/templates/dcim/platform.html

@@ -46,7 +46,7 @@
           <tr>
             <th scope="row">Devices</th>
             <td>
-              <a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ devices_table.rows|length }}</a>
+              <a href="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}">{{ device_count }}</a>
             </td>
           </tr>
           <tr>
@@ -78,10 +78,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Devices</h5>
-      <div class="card-body table-responsive">
-        {% render_table devices_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:device_list' %}?platform_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 36 - 34
netbox/templates/dcim/powerpanel.html

@@ -45,40 +45,42 @@
     </div>
 </div>
 <div class="row my-3">
-    <div class="col col-md-12">
-        <form method="post">
-            {% csrf_token %}
-            <div class="card">
-                <div class="card-body table-responsive">
-                    {% render_table powerfeed_table 'inc/table.html' %}
-                </div>
-                <div class="card-footer noprint">
-                    {% if perms.dcim.change_powerfeed %}
-                        <button type="submit" name="_edit" formaction="{% url 'dcim:powerfeed_bulk_edit' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-warning btn-sm">
-                            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
-                        </button>
-                    {% endif %}
-                    {% if perms.dcim.delete_cable %}
-                        <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerfeed_bulk_disconnect' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                            <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-                        </button>
-                    {% endif %}
-                    {% if perms.dcim.delete_powerfeed %}
-                        <button type="submit" name="_delete" formaction="{% url 'dcim:powerfeed_bulk_delete' %}?return_url={% url 'dcim:powerpanel' 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.dcim.add_powerfeed %}
-                        <div class="float-end">
-                            <a href="{% url 'dcim:powerfeed_add' %}?power_panel={{ object.pk }}&return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-primary btn-sm">
-                                <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Power Feeds
-                            </a>
-                        </div>
-                    {% endif %}
-                 </div>
+  <div class="col col-md-12">
+    <form method="post">
+      {% csrf_token %}
+      <div class="card">
+        <h5 class="card-header">Power Feeds</h5>
+        <div class="card-body htmx-container table-responsive"
+          hx-get="{% url 'dcim:powerfeed_list' %}?power_panel_id={{ object.pk }}"
+          hx-trigger="load"
+        ></div>
+        <div class="card-footer noprint">
+          {% if perms.dcim.change_powerfeed %}
+            <button type="submit" name="_edit" formaction="{% url 'dcim:powerfeed_bulk_edit' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-warning btn-sm">
+              <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
+            </button>
+          {% endif %}
+          {% if perms.dcim.delete_cable %}
+            <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerfeed_bulk_disconnect' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+              <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+            </button>
+          {% endif %}
+          {% if perms.dcim.delete_powerfeed %}
+            <button type="submit" name="_delete" formaction="{% url 'dcim:powerfeed_bulk_delete' %}?return_url={% url 'dcim:powerpanel' 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.dcim.add_powerfeed %}
+            <div class="float-end">
+              <a href="{% url 'dcim:powerfeed_add' %}?power_panel={{ object.pk }}&return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-primary btn-sm">
+                <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Power Feeds
+              </a>
             </div>
-        </form>
-        {% plugin_full_width_page object %}
-    </div>
+          {% endif %}
+        </div>
+      </div>
+    </form>
+    {% plugin_full_width_page object %}
+  </div>
 </div>
 {% endblock %}

+ 5 - 5
netbox/templates/dcim/rackrole.html

@@ -37,7 +37,7 @@
           <tr>
             <th scope="row">Racks</th>
             <td>
-              <a href="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}">{{ racks_table.rows|length }}</a>
+              <a href="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}">{{ object.racks.count }}</a>
             </td>
           </tr>
         </table>
@@ -55,10 +55,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Racks</h5>
-      <div class="card-body table-responsive">
-        {% render_table racks_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=racks_table.paginator page=racks_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:rack_list' %}?role_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 11 - 14
netbox/templates/dcim/region.html

@@ -22,9 +22,7 @@
 <div class="row mb-3">
 	<div class="col col-md-6">
     <div class="card">
-      <h5 class="card-header">
-        Region
-      </h5>
+      <h5 class="card-header">Region</h5>
       <div class="card-body">
         <table class="table table-hover attr-table">
           <tr>
@@ -42,7 +40,7 @@
           <tr>
             <th scope="row">Sites</th>
             <td>
-              <a href="{% url 'dcim:site_list' %}?region_id={{ object.pk }}">{{ sites_table.rows|length }}</a>
+              <a href="{% url 'dcim:site_list' %}?region_id={{ object.pk }}">{{ object.sites.count }}</a>
             </td>
           </tr>
         </table>
@@ -55,12 +53,11 @@
   </div>
 	<div class="col col-md-6">
     <div class="card">
-      <h5 class="card-header">
-        Child Regions
-      </h5>
-      <div class="card-body table-responsive">
-        {% render_table child_regions_table 'inc/table.html' %}
-      </div>
+      <h5 class="card-header">Child Regions</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:region_list' %}?parent_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
       {% if perms.dcim.add_region %}
         <div class="card-footer text-end noprint">
           <a href="{% url 'dcim:region_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
@@ -76,10 +73,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Sites</h5>
-      <div class="card-body table-responsive">
-        {% render_table sites_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:site_list' %}?region_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 11 - 14
netbox/templates/dcim/sitegroup.html

@@ -22,9 +22,7 @@
 <div class="row mb-3">
 	<div class="col col-md-6">
     <div class="card">
-      <h5 class="card-header">
-        Site Group
-      </h5>
+      <h5 class="card-header">Site Group</h5>
       <div class="card-body">
         <table class="table table-hover attr-table">
           <tr>
@@ -42,7 +40,7 @@
           <tr>
             <th scope="row">Sites</th>
             <td>
-              <a href="{% url 'dcim:site_list' %}?group_id={{ object.pk }}">{{ sites_table.rows|length }}</a>
+              <a href="{% url 'dcim:site_list' %}?group_id={{ object.pk }}">{{ object.sites.count }}</a>
             </td>
           </tr>
         </table>
@@ -55,12 +53,11 @@
   </div>
 	<div class="col col-md-6">
     <div class="card">
-      <h5 class="card-header">
-        Child Groups
-      </h5>
-      <div class="card-body table-responsive">
-        {% render_table child_groups_table 'inc/table.html' %}
-      </div>
+      <h5 class="card-header">Child Groups</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:sitegroup_list' %}?parent_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
       {% if perms.dcim.add_sitegroup %}
         <div class="card-footer text-end noprint">
           <a href="{% url 'dcim:sitegroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
@@ -76,10 +73,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Sites</h5>
-      <div class="card-body table-responsive">
-        {% render_table sites_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:site_list' %}?group_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 10 - 4
netbox/templates/dcim/virtualdevicecontext.html

@@ -49,6 +49,12 @@
               {{ object.tenant|linkify|placeholder }}
             </td>
           </tr>
+          <tr>
+            <th scope="row">Interfaces</th>
+            <td>
+              <a href="{% url 'dcim:interface_list' %}?vdc_id={{ object.pk }}">{{ object.interfaces.count }}</a>
+            </td>
+          </tr>
         </table>
       </div>
     </div>
@@ -65,10 +71,10 @@
   <div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Interfaces</h5>
-      <div class="card-body table-responsive">
-        {% render_table interfaces_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=interfaces_table.paginator page=interfaces_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'dcim:interface_list' %}?vdc_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 1 - 1
netbox/templates/generic/object_list.html

@@ -109,7 +109,7 @@ Context:
               {% endif %}
 
           <div class="card">
-            <div class="card-body" id="object_list">
+            <div class="card-body htmx-container table-responsive" id="object_list">
               {% include 'htmx/table.html' %}
             </div>
           </div>

+ 4 - 3
netbox/templates/home.html

@@ -70,9 +70,10 @@
               <i class="mdi mdi-clipboard-clock"></i>
               <span class="ms-1">Change Log</span>
             </h6>
-            <div class="card-body table-responsive">
-              {% render_table changelog_table 'inc/table.html' %}
-            </div>
+            <div class="card-body htmx-container table-responsive"
+              hx-get="{% url 'extras:objectchange_list' %}?sort=-time"
+              hx-trigger="load"
+            ></div>
           </div>
         </div>
       </div>

+ 2 - 2
netbox/templates/htmx/table.html

@@ -4,10 +4,10 @@
 
 {% with preferences|get_key:"pagination.placement" as paginator_placement %}
   {% if paginator_placement == 'top' or paginator_placement == 'both' %}
-    {% include 'inc/paginator_htmx.html' with paginator=table.paginator page=table.page %}
+    {% include 'inc/paginator_htmx.html' with table=table paginator=table.paginator page=table.page %}
   {% endif %}
   {% render_table table 'inc/table_htmx.html' %}
   {% if paginator_placement != 'top' %}
-    {% include 'inc/paginator_htmx.html' with paginator=table.paginator page=table.page %}
+    {% include 'inc/paginator_htmx.html' with table=table paginator=table.paginator page=table.page %}
   {% endif %}
 {% endwith %}

+ 12 - 12
netbox/templates/inc/paginator_htmx.html

@@ -7,9 +7,9 @@
       <div class="btn-group btn-group-sm" role="group" aria-label="Pages">
         {% if page.has_previous %}
           <a href="#"
-             hx-get="{% querystring request page=page.previous_page_number %}"
-             hx-target="#object_list"
-             hx-push-url="true"
+             hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}"
+             hx-target="closest .htmx-container"
+             {% if not table.embedded %}hx-push-url="true"{% endif %}
              class="btn btn-outline-secondary"
           >
             <i class="mdi mdi-chevron-double-left"></i>
@@ -18,9 +18,9 @@
         {% for p in page.smart_pages %}
           {% if p %}
             <a href="#"
-               hx-get="{% querystring request page=p %}"
-               hx-target="#object_list"
-               hx-push-url="true"
+               hx-get="{{ table.htmx_url }}{% querystring request page=p %}"
+               hx-target="closest .htmx-container"
+             {% if not table.embedded %}hx-push-url="true"{% endif %}
                class="btn btn-outline-secondary{% if page.number == p %} active{% endif %}"
             >
               {{ p }}
@@ -33,9 +33,9 @@
         {% endfor %}
         {% if page.has_next %}
           <a href="#"
-             hx-get="{% querystring request page=page.next_page_number %}"
-             hx-target="#object_list"
-             hx-push-url="true"
+             hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}"
+             hx-target="closest .htmx-container"
+             {% if not table.embedded %}hx-push-url="true"{% endif %}
              class="btn btn-outline-secondary"
           >
             <i class="mdi mdi-chevron-double-right"></i>
@@ -55,9 +55,9 @@
           {% for n in page.paginator.get_page_lengths %}
             <li>
               <a href="#"
-                 hx-get="{% querystring request per_page=n %}"
-                 hx-target="#object_list"
-                 hx-push-url="true"
+                 hx-get="{{ table.htmx_url }}{% querystring request per_page=n %}"
+                 hx-target="closest .htmx-container"
+                 {% if not table.embedded %}hx-push-url="true"{% endif %}
                  class="dropdown-item"
               >{{ n }}</a>
             </li>

+ 10 - 12
netbox/templates/inc/panel_table.html

@@ -1,16 +1,14 @@
 {% load render_table from django_tables2 %}
 
-<div class="card {% if panel_class %}bg-{{ panel_class }}{% endif %}">
-    {% if heading %}
-    <h5 class="card-header">
-        {{ heading }}
-    </h5>
+<div class="card {% if panel_class %}border-{{ panel_class }}{% endif %}">
+  {% if heading %}
+    <h5 class="card-header{% if panel_class %} text-{{ panel_class }}{% endif %}">{{ heading }}</h5>
+  {% endif %}
+  <div class="card-body table-responsive">
+    {% if table.rows %}
+      {% render_table table 'inc/table.html' %}
+    {% else %}
+      <div class="text-muted">None</div>
     {% endif %}
-    <div class="card-body table-responsive">
-      {% if table.rows %}
-        {% render_table table 'inc/table.html' %}
-      {% else %}
-        <div class="text-muted">None</div>
-      {% endif %}
-    </div>
+  </div>
 </div>

+ 53 - 56
netbox/templates/inc/table_htmx.html

@@ -1,59 +1,56 @@
 {% load django_tables2 %}
-
-<div class="table-responsive">
-  <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
-    {% if table.show_header %}
-      <thead>
+<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
+  {% if table.show_header %}
+    <thead>
+      <tr>
+        {% for column in table.columns %}
+          {% if column.orderable %}
+            <th {{ column.attrs.th.as_html }}>
+              {% if column.is_ordered %}
+                <div class="float-end">
+                  <a href="#"
+                     hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field='' %}"
+                     hx-target="closest .htmx-container"
+                     {% if not table.embedded %}hx-push-url="true"{% endif %}
+                     class="text-danger"
+                  ><i class="mdi mdi-close"></i></a>
+                </div>
+              {% endif %}
+              <a href="#"
+                 hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
+                 hx-target="closest .htmx-container"
+                 {% if not table.embedded %}hx-push-url="true"{% endif %}
+              >{{ column.header }}</a>
+            </th>
+          {% else %}
+            <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
+          {% endif %}
+        {% endfor %}
+      </tr>
+    </thead>
+  {% endif %}
+  <tbody>
+    {% for row in table.page.object_list|default:table.rows %}
+      <tr {{ row.attrs.as_html }}>
+        {% for column, cell in row.items %}
+          <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
+        {% endfor %}
+      </tr>
+    {% empty %}
+      {% if table.empty_text %}
         <tr>
-          {% for column in table.columns %}
-            {% if column.orderable %}
-              <th {{ column.attrs.th.as_html }}>
-                {% if column.is_ordered %}
-                  <div class="float-end">
-                    <a href="#"
-                       hx-get="{% querystring table.prefixed_order_by_field='' %}"
-                       hx-target="#object_list"
-                       hx-push-url="true"
-                       class="text-danger"
-                    ><i class="mdi mdi-close"></i></a>
-                  </div>
-                {% endif %}
-                <a href="#"
-                   hx-get="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
-                   hx-target="#object_list"
-                   hx-push-url="true"
-                >{{ column.header }}</a>
-              </th>
-            {% else %}
-              <th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
-            {% endif %}
-          {% endfor %}
+          <td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
         </tr>
-      </thead>
-    {% endif %}
-    <tbody>
-      {% for row in table.page.object_list|default:table.rows %}
-        <tr {{ row.attrs.as_html }}>
-          {% for column, cell in row.items %}
-            <td {{ column.attrs.td.as_html }}>{{ cell }}</td>
-          {% endfor %}
-        </tr>
-      {% empty %}
-        {% if table.empty_text %}
-          <tr>
-            <td colspan="{{ table.columns|length }}" class="text-center text-muted">&mdash; {{ table.empty_text }} &mdash;</td>
-          </tr>
-        {% endif %}
-      {% endfor %}
-    </tbody>
-    {% if table.has_footer %}
-      <tfoot>
-        <tr>
-          {% for column in table.columns %}
-            <td>{{ column.footer }}</td>
-          {% endfor %}
-        </tr>
-      </tfoot>
-    {% endif %}
-  </table>
-</div>
+      {% endif %}
+    {% endfor %}
+  </tbody>
+  {% if table.has_footer %}
+    <tfoot>
+      <tr>
+        {% for column in table.columns %}
+          <td>{{ column.footer }}</td>
+        {% endfor %}
+      </tr>
+    </tfoot>
+  {% endif %}
+</table>

+ 1 - 1
netbox/templates/ipam/aggregate/prefixes.html

@@ -18,7 +18,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 8 - 8
netbox/templates/ipam/asn.html

@@ -75,17 +75,17 @@
     <div class="col col-md-12">
       <div class="card">
         <h5 class="card-header">Sites</h5>
-        <div class="card-body table-responsive">
-          {% render_table sites_table 'inc/table.html' %}
-          {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
-        </div>
+        <div class="card-body htmx-container table-responsive"
+          hx-get="{% url 'dcim:site_list' %}?asn_id={{ object.pk }}"
+          hx-trigger="load"
+        ></div>
       </div>
       <div class="card">
         <h5 class="card-header">Providers</h5>
-        <div class="card-body table-responsive">
-          {% render_table providers_table 'inc/table.html' %}
-          {% include 'inc/paginator.html' with paginator=providers_table.paginator page=providers_table.page %}
-        </div>
+        <div class="card-body htmx-container table-responsive"
+          hx-get="{% url 'circuits:provider_list' %}?asn_id={{ object.pk }}"
+          hx-trigger="load"
+        ></div>
       </div>
       {% plugin_full_width_page object %}
     </div>

+ 5 - 17
netbox/templates/ipam/fhrpgroup.html

@@ -69,13 +69,10 @@
     <div class="col col-md-12">
       <div class="card">
         <h5 class="card-header">Virtual IP Addresses</h5>
-        <div class="card-body table-responsive">
-          {% if ipaddress_table.rows %}
-            {% render_table ipaddress_table 'inc/table.html' %}
-          {% else %}
-            <div class="text-muted">None</div>
-          {% endif %}
-        </div>
+        <div class="card-body htmx-container table-responsive"
+          hx-get="{% url 'ipam:ipaddress_list' %}?fhrpgroup_id={{ object.pk }}"
+          hx-trigger="load"
+        ></div>
         {% if perms.ipam.add_ipaddress %}
           <div class="card-footer text-end">
             <a href="{% url 'ipam:ipaddress_add' %}?fhrpgroup={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
@@ -84,16 +81,7 @@
           </div>
         {% endif %}
       </div>
-      <div class="card">
-        <h5 class="card-header">Members</h5>
-        <div class="card-body table-responsive">
-          {% if members_table.rows %}
-            {% render_table members_table 'inc/table.html' %}
-          {% else %}
-            <div class="text-muted">None</div>
-          {% endif %}
-        </div>
-      </div>
+      {% include 'inc/panel_table.html' with table=members_table heading='Members' %}
       {% plugin_full_width_page object %}
     </div>
   </div>

+ 2 - 23
netbox/templates/ipam/ipaddress.html

@@ -114,30 +114,9 @@
 	<div class="col col-md-8">
     {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
     {% if duplicate_ips_table.rows %}
-        {# Custom version of panel_table.html #}
-        <div class="card border-danger">
-            <h5 class="card-header">
-              <span class="text-danger">Duplicate IP Addresses</span>
-                {% if more_duplicate_ips %}
-                  <div class="float-end">
-                    <a type="button" class="btn btn-primary btn-sm"
-                    {% if object.vrf %}
-                    href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id={{ object.vrf.pk }}"
-                    {% else %}
-                    href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id=null"
-                    {% endif %}
-                    >Show all</a>
-                  </div>
-                {% endif %}
-            </h5>
-            <div class="card-body table-responsive">
-              {% render_table duplicate_ips_table 'inc/table.html' %}
-            </div>
-        </div>
+      {% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
     {% endif %}
-    <div class="my-3">
-      {% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
-    </div>
+    {% include 'inc/panel_table.html' with table=related_ips_table heading='Related IPs' %}
     {% include 'inc/panels/services.html' %}
     {% plugin_right_page object %}
 	</div>

+ 1 - 1
netbox/templates/ipam/iprange/ip_addresses.html

@@ -16,7 +16,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 4 - 3
netbox/templates/ipam/l2vpn.html

@@ -55,9 +55,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Terminations</h5>
-      <div class="card-body">
-        {% render_table terminations_table 'inc/table.html' %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'ipam:l2vpntermination_list' %}?l2vpn_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
       {% if perms.ipam.add_l2vpntermination %}
         <div class="card-footer text-end noprint">
           <a href="{% url 'ipam:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm{% if not object.can_add_termination %} disabled" aria-disabled="true{% endif %}">

+ 1 - 1
netbox/templates/ipam/prefix/ip_addresses.html

@@ -16,7 +16,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/ipam/prefix/ip_ranges.html

@@ -16,7 +16,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/ipam/prefix/prefixes.html

@@ -18,7 +18,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 6 - 6
netbox/templates/ipam/rir.html

@@ -35,16 +35,16 @@
           <tr>
             <th scope="row">Aggregates</th>
             <td>
-              <a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}">{{ aggregates_table.rows|length }}</a>
+              <a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}">{{ object.aggregates.count }}</a>
             </td>
           </tr>
         </table>
       </div>
     </div>
-    {% include 'inc/panels/tags.html' %}
     {% plugin_left_page object %}
 	</div>
 	<div class="col col-md-6">
+    {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% plugin_right_page object %}
   </div>
@@ -53,10 +53,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Aggregates</h5>
-      <div class="card-body table-responsive">
-        {% render_table aggregates_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=aggregates_table.paginator page=aggregates_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'ipam:aggregate_list' %}?rir_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 8 - 20
netbox/templates/ipam/role.html

@@ -35,40 +35,28 @@
           <tr>
             <th scope="row">Prefixes</th>
             <td>
-              <a href="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}">{{ prefixes_table.rows|length }}</a>
+              <a href="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}">{{ object.prefixes.count }}</a>
             </td>
           </tr>
           <tr>
             <th scope="row">IP Ranges</th>
             <td>
-              {% with ipranges_count=object.ip_ranges.count %}
-                {% if ipranges_count %}
-                  <a href="{% url 'ipam:iprange_list' %}?role_id={{ object.pk }}">{{ ipranges_count }}</a>
-                {% else %}
-                  {{ ''|placeholder }}
-                {% endif %}
-              {% endwith %}
+              <a href="{% url 'ipam:iprange_list' %}?role_id={{ object.pk }}">{{ object.ip_ranges.count }}</a>
             </td>
           </tr>
           <tr>
             <th scope="row">VLANs</th>
             <td>
-              {% with vlans_count=object.vlans.count %}
-                {% if vlans_count %}
-                  <a href="{% url 'ipam:vlan_list' %}?role_id={{ object.pk }}">{{ vlans_count }}</a>
-                {% else %}
-                  {{ ''|placeholder }}
-                {% endif %}
-              {% endwith %}
+              <a href="{% url 'ipam:vlan_list' %}?role_id={{ object.pk }}">{{ object.vlans.count }}</a>
             </td>
           </tr>
         </table>
       </div>
     </div>
-    {% include 'inc/panels/tags.html' %}
     {% plugin_left_page object %}
 	</div>
 	<div class="col col-md-6">
+    {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% plugin_right_page object %}
   </div>
@@ -77,10 +65,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Prefixes</h5>
-      <div class="card-body table-responsive">
-        {% render_table prefixes_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=prefixes_table.paginator page=prefixes_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'ipam:prefix_list' %}?role_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 15 - 17
netbox/templates/ipam/vlan.html

@@ -81,24 +81,22 @@
         </div>
     </div>
     <div class="row">
-        <div class="col col-md-12">
-            <div class="card">
-                <h5 class="card-header">
-                    Prefixes
-                </h5>
-                <div class="card-body table-responsive">
-                    {% render_table prefix_table 'inc/table.html' %}
-                </div>
-                {% if perms.ipam.add_prefix %}
-                <div class="card-footer text-end noprint">
-                    <a href="{% url 'ipam:prefix_add' %}?{% if object.tenant %}tenant={{ object.tenant.pk }}&{% endif %}site={{ object.site.pk }}&vlan={{ object.pk }}" class="btn btn-primary btn-sm">
-                        <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
-                        Add a Prefix
-                    </a>
-                </div>
-                {% endif %}
+      <div class="col col-md-12">
+        <div class="card">
+          <h5 class="card-header">Prefixes</h5>
+          <div class="card-body htmx-container table-responsive"
+            hx-get="{% url 'ipam:prefix_list' %}?vlan_id={{ object.pk }}"
+            hx-trigger="load"
+          ></div>
+          {% if perms.ipam.add_prefix %}
+            <div class="card-footer text-end noprint">
+              <a href="{% url 'ipam:prefix_add' %}?{% if object.tenant %}tenant={{ object.tenant.pk }}&{% endif %}site={{ object.site.pk }}&vlan={{ object.pk }}" class="btn btn-primary btn-sm">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Prefix
+              </a>
             </div>
-            {% plugin_full_width_page object %}
+          {% endif %}
         </div>
+        {% plugin_full_width_page object %}
+      </div>
     </div>
 {% endblock %}

+ 1 - 1
netbox/templates/ipam/vlan/interfaces.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/ipam/vlan/vminterfaces.html

@@ -7,7 +7,7 @@
   <form method="post">
     {% csrf_token %}
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/search.html

@@ -30,7 +30,7 @@
   </div>
   <div class="row px-3">
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 14 - 17
netbox/templates/tenancy/contactgroup.html

@@ -34,7 +34,7 @@
             <tr>
               <th scope="row">Contacts</th>
               <td>
-                <a href="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}">{{ contacts_table.rows|length }}</a>
+                <a href="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}">{{ object.contacts.count }}</a>
               </td>
             </tr>
           </table>
@@ -46,12 +46,11 @@
     <div class="col col-md-6">
       {% include 'inc/panels/custom_fields.html' %}
       <div class="card">
-        <h5 class="card-header">
-          Child Groups
-        </h5>
-        <div class="card-body table-responsive">
-          {% render_table child_groups_table 'inc/table.html' %}
-        </div>
+        <h5 class="card-header">Child Groups</h5>
+        <div class="card-body htmx-container table-responsive"
+          hx-get="{% url 'tenancy:contactgroup_list' %}?parent_id={{ object.pk }}"
+          hx-trigger="load"
+        ></div>
         {% if perms.tenancy.add_contactgroup %}
           <div class="card-footer text-end noprint">
             <a href="{% url 'tenancy:contactgroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
@@ -63,16 +62,14 @@
       {% plugin_right_page object %}
     </div>
   </div>
-  <div class="row mb-3">
-    <div class="col col-md-12">
-      <div class="card">
-        <h5 class="card-header">Contacts</h5>
-        <div class="card-body table-responsive">
-          {% render_table contacts_table 'inc/table.html' %}
-          {% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
-        </div>
-      </div>
-      {% plugin_full_width_page object %}
+  <div class="col col-md-12">
+    <div class="card">
+      <h5 class="card-header">Contacts</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'tenancy:contact_list' %}?group_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
+    {% plugin_full_width_page object %}
   </div>
 {% endblock %}

+ 19 - 5
netbox/templates/tenancy/tenantgroup.html

@@ -42,7 +42,7 @@
           <tr>
             <th scope="row">Tenants</th>
             <td>
-              <a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ tenants_table.rows|length }}</a>
+              <a href="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}">{{ object.tenants.count }}</a>
             </td>
           </tr>
         </table>
@@ -53,6 +53,20 @@
   </div>
 	<div class="col col-md-6">
     {% include 'inc/panels/custom_fields.html' %}
+    <div class="card">
+      <h5 class="card-header">Child Groups</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'tenancy:tenantgroup_list' %}?parent_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
+      {% if perms.tenancy.add_tenantgroup %}
+        <div class="card-footer text-end noprint">
+          <a href="{% url 'tenancy:tenantgroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Tenant Group
+          </a>
+        </div>
+      {% endif %}
+    </div>
     {% plugin_right_page object %}
 	</div>
 </div>
@@ -60,10 +74,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Tenants</h5>
-      <div class="card-body table-responsive">
-        {% render_table tenants_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=tenants_table.paginator page=tenants_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'tenancy:tenant_list' %}?group_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 1 - 1
netbox/templates/virtualization/cluster/devices.html

@@ -8,7 +8,7 @@
   <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
     {% csrf_token %}
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 1 - 1
netbox/templates/virtualization/cluster/virtual_machines.html

@@ -8,7 +8,7 @@
   <form method="post">
     {% csrf_token %}
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 5 - 5
netbox/templates/virtualization/clustergroup.html

@@ -31,7 +31,7 @@
           <tr>
             <th scope="row">Clusters</th>
             <td>
-              <a href="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}">{{ clusters_table.rows|length }}</a>
+              <a href="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}">{{ object.clusters.count }}</a>
             </td>
           </tr>
         </table>
@@ -50,10 +50,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Clusters</h5>
-      <div class="card-body table-responsive">
-        {% render_table clusters_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'virtualization:cluster_list' %}?group_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 5 - 5
netbox/templates/virtualization/clustertype.html

@@ -31,7 +31,7 @@
           <tr>
             <th scope="row">Clusters</th>
             <td>
-              <a href="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}">{{ clusters_table.rows|length }}</a>
+              <a href="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}">{{ object.clusters.count }}</a>
             </td>
           </tr>
         </table>
@@ -49,10 +49,10 @@
 	<div class="col col-md-12">
     <div class="card">
       <h5 class="card-header">Clusters</h5>
-      <div class="card-body table-responsive">
-        {% render_table clusters_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
-      </div>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'virtualization:cluster_list' %}?type_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 1 - 1
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -9,7 +9,7 @@
     {% csrf_token %}
 
     <div class="card">
-      <div class="card-body" id="object_list">
+      <div class="card-body htmx-container table-responsive" id="object_list">
         {% include 'htmx/table.html' %}
       </div>
     </div>

+ 5 - 10
netbox/templates/virtualization/vminterface.html

@@ -80,16 +80,11 @@
 <div class="row mb-3">
     <div class="col col-md-12">
         <div class="card">
-            <h5 class="card-header">
-                IP Addresses
-            </h5>
-            <div class="card-body table-responsive">
-                {% if ipaddress_table.rows %}
-                    {% render_table ipaddress_table 'inc/table.html' %}
-                {% else %}
-                    <div class="text-muted">None</div>
-                {% endif %}
-            </div>
+            <h5 class="card-header">IP Addresses</h5>
+            <div class="card-body htmx-container table-responsive"
+              hx-get="{% url 'ipam:ipaddress_list' %}?vminterface_id={{ object.pk }}"
+              hx-trigger="load"
+            ></div>
             {% if perms.ipam.add_ipaddress %}
                 <div class="card-footer text-end noprint">
                     <a href="{% url 'ipam:ipaddress_add' %}?virtual_machine={{ object.virtual_machine.pk }}&vminterface={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">

+ 20 - 6
netbox/templates/wireless/wirelesslangroup.html

@@ -40,7 +40,7 @@
           <tr>
             <th scope="row">Wireless LANs</th>
             <td>
-              <a href="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}">{{ wirelesslans_table.rows|length }}</a>
+              <a href="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}">{{ object.wirelesslans.count }}</a>
             </td>
           </tr>
         </table>
@@ -51,17 +51,31 @@
   </div>
 	<div class="col col-md-6">
     {% include 'inc/panels/custom_fields.html' %}
+    <div class="card">
+      <h5 class="card-header">Child Groups</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'wireless:wirelesslangroup_list' %}?parent_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
+      {% if perms.wireless.add_wirelesslangroup %}
+        <div class="card-footer text-end noprint">
+          <a href="{% url 'wireless:wirelesslangroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
+            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Wireless LAN Group
+          </a>
+        </div>
+      {% endif %}
+    </div>
     {% plugin_right_page object %}
 	</div>
 </div>
 <div class="row mb-3">
 	<div class="col col-md-12">
     <div class="card">
-      <div class="card-header">Wireless LANs</div>
-      <div class="card-body table-responsive">
-        {% render_table wirelesslans_table 'inc/table.html' %}
-        {% include 'inc/paginator.html' with paginator=wirelesslans_table.paginator page=wirelesslans_table.page %}
-      </div>
+      <h5 class="card-header">Wireless LANs</h5>
+      <div class="card-body htmx-container table-responsive"
+        hx-get="{% url 'wireless:wirelesslan_list' %}?group_id={{ object.pk }}"
+        hx-trigger="load"
+      ></div>
     </div>
     {% plugin_full_width_page object %}
   </div>

+ 0 - 37
netbox/tenancy/views.py

@@ -34,17 +34,6 @@ class TenantGroupListView(generic.ObjectListView):
 class TenantGroupView(generic.ObjectView):
     queryset = TenantGroup.objects.all()
 
-    def get_extra_context(self, request, instance):
-        tenants = Tenant.objects.restrict(request.user, 'view').filter(
-            group=instance
-        )
-        tenants_table = tables.TenantTable(tenants, user=request.user, exclude=('group',))
-        tenants_table.configure(request)
-
-        return {
-            'tenants_table': tenants_table,
-        }
-
 
 @register_model_view(TenantGroup, 'edit')
 class TenantGroupEditView(generic.ObjectEditView):
@@ -182,32 +171,6 @@ class ContactGroupListView(generic.ObjectListView):
 class ContactGroupView(generic.ObjectView):
     queryset = ContactGroup.objects.all()
 
-    def get_extra_context(self, request, instance):
-        child_groups = ContactGroup.objects.add_related_count(
-            ContactGroup.objects.all(),
-            Contact,
-            'group',
-            'contact_count',
-            cumulative=True
-        ).restrict(request.user, 'view').filter(
-            parent__in=instance.get_descendants(include_self=True)
-        )
-        child_groups_table = tables.ContactGroupTable(child_groups)
-        child_groups_table.columns.hide('actions')
-
-        contacts = Contact.objects.restrict(request.user, 'view').filter(
-            group=instance
-        ).annotate(
-            assignment_count=count_related(ContactAssignment, 'contact')
-        )
-        contacts_table = tables.ContactTable(contacts, user=request.user, exclude=('group',))
-        contacts_table.configure(request)
-
-        return {
-            'child_groups_table': child_groups_table,
-            'contacts_table': contacts_table,
-        }
-
 
 @register_model_view(ContactGroup, 'edit')
 class ContactGroupEditView(generic.ObjectEditView):

+ 14 - 0
netbox/utilities/htmx.py

@@ -1,5 +1,19 @@
+from urllib.parse import urlparse
+
+
 def is_htmx(request):
     """
     Returns True if the request was made by HTMX; False otherwise.
     """
     return 'Hx-Request' in request.headers
+
+
+def is_embedded(request):
+    """
+    Returns True if the request indicates that it originates from a URL different from
+    the path being requested.
+    """
+    hx_current_url = request.headers.get('HX-Current-URL', None)
+    if not hx_current_url:
+        return False
+    return request.path != urlparse(hx_current_url).path

+ 0 - 20
netbox/virtualization/views.py

@@ -100,20 +100,6 @@ class ClusterGroupListView(generic.ObjectListView):
 class ClusterGroupView(generic.ObjectView):
     queryset = ClusterGroup.objects.all()
 
-    def get_extra_context(self, request, instance):
-        clusters = Cluster.objects.restrict(request.user, 'view').filter(
-            group=instance
-        ).annotate(
-            device_count=count_related(Device, 'cluster'),
-            vm_count=count_related(VirtualMachine, 'cluster')
-        )
-        clusters_table = tables.ClusterTable(clusters, user=request.user, exclude=('group',))
-        clusters_table.configure(request)
-
-        return {
-            'clusters_table': clusters_table,
-        }
-
 
 @register_model_view(ClusterGroup, 'edit')
 class ClusterGroupEditView(generic.ObjectEditView):
@@ -444,11 +430,6 @@ class VMInterfaceView(generic.ObjectView):
     queryset = VMInterface.objects.all()
 
     def get_extra_context(self, request, instance):
-        # Get assigned IP addresses
-        ipaddress_table = AssignedIPAddressesTable(
-            data=instance.ip_addresses.restrict(request.user, 'view'),
-            orderable=False
-        )
 
         # Get child interfaces
         child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance)
@@ -473,7 +454,6 @@ class VMInterfaceView(generic.ObjectView):
         )
 
         return {
-            'ipaddress_table': ipaddress_table,
             'child_interfaces_table': child_interfaces_tables,
             'vlan_table': vlan_table,
         }

+ 0 - 11
netbox/wireless/views.py

@@ -27,17 +27,6 @@ class WirelessLANGroupListView(generic.ObjectListView):
 class WirelessLANGroupView(generic.ObjectView):
     queryset = WirelessLANGroup.objects.all()
 
-    def get_extra_context(self, request, instance):
-        wirelesslans = WirelessLAN.objects.restrict(request.user, 'view').filter(
-            group=instance
-        )
-        wirelesslans_table = tables.WirelessLANTable(wirelesslans, user=request.user, exclude=('group',))
-        wirelesslans_table.configure(request)
-
-        return {
-            'wirelesslans_table': wirelesslans_table,
-        }
-
 
 @register_model_view(WirelessLANGroup, 'edit')
 class WirelessLANGroupEditView(generic.ObjectEditView):