Selaa lähdekoodia

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 vuotta sitten
vanhempi
commit
1a2dae3471
65 muutettua tiedostoa jossa 381 lisäystä ja 667 poistoa
  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):
 class ProviderView(generic.ObjectView):
     queryset = Provider.objects.all()
     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')
 @register_model_view(Provider, 'edit')
 class ProviderEditView(generic.ObjectEditView):
 class ProviderEditView(generic.ObjectEditView):
@@ -93,21 +79,6 @@ class ProviderNetworkListView(generic.ObjectListView):
 class ProviderNetworkView(generic.ObjectView):
 class ProviderNetworkView(generic.ObjectView):
     queryset = ProviderNetwork.objects.all()
     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')
 @register_model_view(ProviderNetwork, 'edit')
 class ProviderNetworkEditView(generic.ObjectEditView):
 class ProviderNetworkEditView(generic.ObjectEditView):
@@ -156,15 +127,6 @@ class CircuitTypeListView(generic.ObjectListView):
 class CircuitTypeView(generic.ObjectView):
 class CircuitTypeView(generic.ObjectView):
     queryset = CircuitType.objects.all()
     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')
 @register_model_view(CircuitType, 'edit')
 class CircuitTypeEditView(generic.ObjectEditView):
 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 circuits.models import Circuit, CircuitTermination
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
 from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
 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 netbox.views import generic
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -212,30 +212,6 @@ class RegionListView(generic.ObjectListView):
 class RegionView(generic.ObjectView):
 class RegionView(generic.ObjectView):
     queryset = Region.objects.all()
     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')
 @register_model_view(Region, 'edit')
 class RegionEditView(generic.ObjectEditView):
 class RegionEditView(generic.ObjectEditView):
@@ -300,30 +276,6 @@ class SiteGroupListView(generic.ObjectListView):
 class SiteGroupView(generic.ObjectView):
 class SiteGroupView(generic.ObjectView):
     queryset = SiteGroup.objects.all()
     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')
 @register_model_view(SiteGroup, 'edit')
 class SiteGroupEditView(generic.ObjectEditView):
 class SiteGroupEditView(generic.ObjectEditView):
@@ -493,22 +445,6 @@ class LocationView(generic.ObjectView):
         rack_count = Rack.objects.filter(location__in=location_ids).count()
         rack_count = Rack.objects.filter(location__in=location_ids).count()
         device_count = Device.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(
         nonracked_devices = Device.objects.filter(
             location=instance,
             location=instance,
             rack__isnull=True,
             rack__isnull=True,
@@ -518,7 +454,6 @@ class LocationView(generic.ObjectView):
         return {
         return {
             'rack_count': rack_count,
             'rack_count': rack_count,
             'device_count': device_count,
             'device_count': device_count,
-            'child_locations_table': child_locations_table,
             'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
             'nonracked_devices': nonracked_devices.order_by('-pk')[:10],
             'total_nonracked_devices_count': nonracked_devices.count(),
             'total_nonracked_devices_count': nonracked_devices.count(),
         }
         }
@@ -583,20 +518,6 @@ class RackRoleListView(generic.ObjectListView):
 class RackRoleView(generic.ObjectView):
 class RackRoleView(generic.ObjectView):
     queryset = RackRole.objects.all()
     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')
 @register_model_view(RackRole, 'edit')
 class RackRoleEditView(generic.ObjectEditView):
 class RackRoleEditView(generic.ObjectEditView):
@@ -859,8 +780,6 @@ class ManufacturerView(generic.ObjectView):
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
         device_types = DeviceType.objects.restrict(request.user, 'view').filter(
         device_types = DeviceType.objects.restrict(request.user, 'view').filter(
             manufacturer=instance
             manufacturer=instance
-        ).annotate(
-            instance_count=count_related(Device, 'device_type')
         )
         )
         module_types = ModuleType.objects.restrict(request.user, 'view').filter(
         module_types = ModuleType.objects.restrict(request.user, 'view').filter(
             manufacturer=instance
             manufacturer=instance
@@ -869,13 +788,10 @@ class ManufacturerView(generic.ObjectView):
             manufacturer=instance
             manufacturer=instance
         )
         )
 
 
-        devicetypes_table = tables.DeviceTypeTable(device_types, user=request.user, exclude=('manufacturer',))
-        devicetypes_table.configure(request)
-
         return {
         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):
 class DeviceRoleView(generic.ObjectView):
     queryset = DeviceRole.objects.all()
     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')
 @register_model_view(DeviceRole, 'devices', path='devices')
 class DeviceRoleDevicesView(generic.ObjectChildrenView):
 class DeviceRoleDevicesView(generic.ObjectChildrenView):
@@ -1833,12 +1736,13 @@ class PlatformView(generic.ObjectView):
         devices = Device.objects.restrict(request.user, 'view').filter(
         devices = Device.objects.restrict(request.user, 'view').filter(
             platform=instance
             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 {
         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
             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
         # Get bridge interfaces
         bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
         bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
         bridge_interfaces_tables = tables.InterfaceTable(
         bridge_interfaces_tables = tables.InterfaceTable(
@@ -2558,7 +2456,6 @@ class InterfaceView(generic.ObjectView):
 
 
         return {
         return {
             'vdc_table': vdc_table,
             'vdc_table': vdc_table,
-            'ipaddress_table': ipaddress_table,
             'bridge_interfaces_table': bridge_interfaces_tables,
             'bridge_interfaces_table': bridge_interfaces_tables,
             'child_interfaces_table': child_interfaces_tables,
             'child_interfaces_table': child_interfaces_tables,
             'vlan_table': vlan_table,
             'vlan_table': vlan_table,
@@ -3533,20 +3430,6 @@ class PowerPanelListView(generic.ObjectListView):
 class PowerPanelView(generic.ObjectView):
 class PowerPanelView(generic.ObjectView):
     queryset = PowerPanel.objects.all()
     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')
 @register_model_view(PowerPanel, 'edit')
 class PowerPanelEditView(generic.ObjectEditView):
 class PowerPanelEditView(generic.ObjectEditView):
@@ -3648,16 +3531,6 @@ class VirtualDeviceContextListView(generic.ObjectListView):
 class VirtualDeviceContextView(generic.ObjectView):
 class VirtualDeviceContextView(generic.ObjectView):
     queryset = VirtualDeviceContext.objects.all()
     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')
 @register_model_view(VirtualDeviceContext, 'edit')
 class VirtualDeviceContextEditView(generic.ObjectEditView):
 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.urls import reverse
 from django.utils.translation import gettext as _
 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.filtersets import InterfaceFilterSet
 from dcim.models import Interface, Site, Device
 from dcim.models import Interface, Site, Device
-from dcim.tables import SiteTable
 from netbox.views import generic
 from netbox.views import generic
 from utilities.utils import count_related
 from utilities.utils import count_related
 from utilities.views import ViewTab, register_model_view
 from utilities.views import ViewTab, register_model_view
@@ -167,17 +165,6 @@ class RIRListView(generic.ObjectListView):
 class RIRView(generic.ObjectView):
 class RIRView(generic.ObjectView):
     queryset = RIR.objects.all()
     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')
 @register_model_view(RIR, 'edit')
 class RIREditView(generic.ObjectEditView):
 class RIREditView(generic.ObjectEditView):
@@ -232,22 +219,11 @@ class ASNView(generic.ObjectView):
     queryset = ASN.objects.all()
     queryset = ASN.objects.all()
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
-        # Gather assigned Sites
         sites = instance.sites.restrict(request.user, 'view')
         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 {
         return {
-            'sites_table': sites_table,
             'sites_count': sites.count(),
             'sites_count': sites.count(),
-            'providers_table': providers_table,
             'providers_count': providers.count(),
             'providers_count': providers.count(),
         }
         }
 
 
@@ -392,18 +368,6 @@ class RoleListView(generic.ObjectListView):
 class RoleView(generic.ObjectView):
 class RoleView(generic.ObjectView):
     queryset = Role.objects.all()
     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')
 @register_model_view(Role, 'edit')
 class RoleEditView(generic.ObjectEditView):
 class RoleEditView(generic.ObjectEditView):
@@ -750,7 +714,6 @@ class IPAddressView(generic.ObjectView):
         return {
         return {
             'parent_prefixes_table': parent_prefixes_table,
             'parent_prefixes_table': parent_prefixes_table,
             'duplicate_ips_table': duplicate_ips_table,
             'duplicate_ips_table': duplicate_ips_table,
-            'more_duplicate_ips': duplicate_ips.count() > 10,
             'related_ips_table': related_ips_table,
             'related_ips_table': related_ips_table,
             'services': services,
             'services': services,
         }
         }
@@ -888,17 +851,9 @@ class VLANGroupView(generic.ObjectView):
             vlans_table.columns.show('pk')
             vlans_table.columns.show('pk')
         vlans_table.configure(request)
         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 {
         return {
             'vlans_count': vlans_count,
             'vlans_count': vlans_count,
             'vlans_table': vlans_table,
             'vlans_table': vlans_table,
-            'permissions': permissions,
         }
         }
 
 
 
 
@@ -954,11 +909,6 @@ class FHRPGroupView(generic.ObjectView):
     queryset = FHRPGroup.objects.all()
     queryset = FHRPGroup.objects.all()
 
 
     def get_extra_context(self, request, instance):
     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
         # Get assigned interfaces
         members_table = tables.FHRPGroupAssignmentTable(
         members_table = tables.FHRPGroupAssignmentTable(
@@ -968,7 +918,6 @@ class FHRPGroupView(generic.ObjectView):
         members_table.columns.hide('group')
         members_table.columns.hide('group')
 
 
         return {
         return {
-            'ipaddress_table': ipaddress_table,
             'members_table': members_table,
             'members_table': members_table,
             'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
             'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
         }
         }
@@ -1250,10 +1199,6 @@ class L2VPNView(generic.ObjectView):
     queryset = L2VPN.objects.all()
     queryset = L2VPN.objects.all()
 
 
     def get_extra_context(self, request, instance):
     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(
         import_targets_table = tables.RouteTargetTable(
             instance.import_targets.prefetch_related('tenant'),
             instance.import_targets.prefetch_related('tenant'),
             orderable=False
             orderable=False
@@ -1264,7 +1209,6 @@ class L2VPNView(generic.ObjectView):
         )
         )
 
 
         return {
         return {
-            'terminations_table': terminations_table,
             'import_targets_table': import_targets_table,
             'import_targets_table': import_targets_table,
             'export_targets_table': export_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.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldDoesNotExist
 from django.core.exceptions import FieldDoesNotExist
 from django.db.models.fields.related import RelatedField
 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.safestring import mark_safe
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext as _
 from django_tables2.data import TableQuerysetData
 from django_tables2.data import TableQuerysetData
@@ -12,7 +14,7 @@ from extras.models import CustomField, CustomLink
 from extras.choices import CustomFieldVisibilityChoices
 from extras.choices import CustomFieldVisibilityChoices
 from netbox.tables import columns
 from netbox.tables import columns
 from utilities.paginator import EnhancedPaginator, get_paginate_count
 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__ = (
 __all__ = (
     'BaseTable',
     'BaseTable',
@@ -197,6 +199,19 @@ class NetBoxTable(BaseTable):
 
 
         super().__init__(*args, extra_columns=extra_columns, **kwargs)
         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):
 class SearchTable(tables.Table):
     object_type = columns.ContentTypeColumn(
     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.error_handlers import handle_protectederror
 from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
 from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
 from utilities.forms import BulkRenameForm, ConfirmationForm, ImportForm, restrict_form_fields
 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.permissions import get_permission_for_model
 from utilities.views import GetReturnURLMixin
 from utilities.views import GetReturnURLMixin
 from .base import BaseMultiObjectView
 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 this is an HTMX request, return only the rendered table HTML
         if is_htmx(request):
         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', {
             return render(request, 'htmx/table.html', {
                 'table': table,
                 'table': table,
             })
             })

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

@@ -31,7 +31,7 @@
           <tr>
           <tr>
             <th scope="row">Circuits</th>
             <th scope="row">Circuits</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -49,10 +49,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Circuits</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -40,7 +40,7 @@
                     <tr>
                     <tr>
                         <th scope="row">Circuits</th>
                         <th scope="row">Circuits</th>
                         <td>
                         <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>
                         </td>
                     </tr>
                     </tr>
                 </table>
                 </table>
@@ -60,10 +60,10 @@
   <div class="col col-md-12">
   <div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Circuits</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -50,10 +50,10 @@
   <div class="col col-md-12">
   <div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Circuits</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </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">
     <div class="tab-pane show active" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
       {% include 'inc/table_controls_htmx.html' %}
       {% include 'inc/table_controls_htmx.html' %}
       <div class="card">
       <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' %}
           {% include 'htmx/table.html' %}
         </div>
         </div>
       </div>
       </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
   {% csrf_token %}
   {% csrf_token %}
 
 
   <div class="card">
   <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' %}
       {% include 'htmx/table.html' %}
     </div>
     </div>
   </div>
   </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -10,7 +10,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -45,14 +45,14 @@
           <tr>
           <tr>
             <th scope="row">Devices</th>
             <th scope="row">Devices</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th>Virtual Machines</th>
             <th>Virtual Machines</th>
             <td>
             <td>
               {% if object.vm_role %}
               {% 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 %}
               {% else %}
                 {{ ''|placeholder }}
                 {{ ''|placeholder }}
               {% endif %}
               {% endif %}

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

@@ -8,7 +8,7 @@
         {% csrf_token %}
         {% csrf_token %}
         <div class="card">
         <div class="card">
             <h5 class="card-header">{{ title }}</h5>
             <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' %}
               {% include 'htmx/table.html' %}
             </div>
             </div>
             <div class="card-footer noprint">
             <div class="card-footer noprint">
@@ -36,7 +36,7 @@
   {% else %}
   {% else %}
     <div class="card">
     <div class="card">
       <h5 class="card-header">{{ title }}</h5>
       <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -344,13 +344,10 @@
     <div class="col col-md-12">
     <div class="col col-md-12">
       <div class="card">
       <div class="card">
         <h5 class="card-header">IP Addresses</h5>
         <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 %}
         {% if perms.ipam.add_ipaddress %}
           <div class="card-footer text-end noprint">
           <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">
             <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="row mb-3">
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -45,19 +45,19 @@
           <tr>
           <tr>
             <th scope="row">Device types</th>
             <th scope="row">Device types</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th scope="row">Module types</th>
             <th scope="row">Module types</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th scope="row">Inventory Items</th>
             <th scope="row">Inventory Items</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -76,10 +76,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Device Types</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -8,7 +8,7 @@
         {% csrf_token %}
         {% csrf_token %}
         <div class="card">
         <div class="card">
             <h5 class="card-header">{{ title }}</h5>
             <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' %}
               {% include 'htmx/table.html' %}
             </div>
             </div>
             <div class="card-footer noprint">
             <div class="card-footer noprint">
@@ -36,7 +36,7 @@
   {% else %}
   {% else %}
     <div class="card">
     <div class="card">
       <h5 class="card-header">{{ title }}</h5>
       <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -46,7 +46,7 @@
           <tr>
           <tr>
             <th scope="row">Devices</th>
             <th scope="row">Devices</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
           <tr>
           <tr>
@@ -78,10 +78,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Devices</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -45,40 +45,42 @@
     </div>
     </div>
 </div>
 </div>
 <div class="row my-3">
 <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>
             </div>
-        </form>
-        {% plugin_full_width_page object %}
-    </div>
+          {% endif %}
+        </div>
+      </div>
+    </form>
+    {% plugin_full_width_page object %}
+  </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

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

@@ -37,7 +37,7 @@
           <tr>
           <tr>
             <th scope="row">Racks</th>
             <th scope="row">Racks</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -55,10 +55,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Racks</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -22,9 +22,7 @@
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     <div class="card">
     <div class="card">
-      <h5 class="card-header">
-        Region
-      </h5>
+      <h5 class="card-header">Region</h5>
       <div class="card-body">
       <div class="card-body">
         <table class="table table-hover attr-table">
         <table class="table table-hover attr-table">
           <tr>
           <tr>
@@ -42,7 +40,7 @@
           <tr>
           <tr>
             <th scope="row">Sites</th>
             <th scope="row">Sites</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -55,12 +53,11 @@
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     <div class="card">
     <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 %}
       {% if perms.dcim.add_region %}
         <div class="card-footer text-end noprint">
         <div class="card-footer text-end noprint">
           <a href="{% url 'dcim:region_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
           <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="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Sites</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -22,9 +22,7 @@
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     <div class="card">
     <div class="card">
-      <h5 class="card-header">
-        Site Group
-      </h5>
+      <h5 class="card-header">Site Group</h5>
       <div class="card-body">
       <div class="card-body">
         <table class="table table-hover attr-table">
         <table class="table table-hover attr-table">
           <tr>
           <tr>
@@ -42,7 +40,7 @@
           <tr>
           <tr>
             <th scope="row">Sites</th>
             <th scope="row">Sites</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -55,12 +53,11 @@
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     <div class="card">
     <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 %}
       {% if perms.dcim.add_sitegroup %}
         <div class="card-footer text-end noprint">
         <div class="card-footer text-end noprint">
           <a href="{% url 'dcim:sitegroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
           <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="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Sites</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -49,6 +49,12 @@
               {{ object.tenant|linkify|placeholder }}
               {{ object.tenant|linkify|placeholder }}
             </td>
             </td>
           </tr>
           </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>
         </table>
       </div>
       </div>
     </div>
     </div>
@@ -65,10 +71,10 @@
   <div class="col col-md-12">
   <div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Interfaces</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -109,7 +109,7 @@ Context:
               {% endif %}
               {% endif %}
 
 
           <div class="card">
           <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' %}
               {% include 'htmx/table.html' %}
             </div>
             </div>
           </div>
           </div>

+ 4 - 3
netbox/templates/home.html

@@ -70,9 +70,10 @@
               <i class="mdi mdi-clipboard-clock"></i>
               <i class="mdi mdi-clipboard-clock"></i>
               <span class="ms-1">Change Log</span>
               <span class="ms-1">Change Log</span>
             </h6>
             </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>
         </div>
       </div>
       </div>

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

@@ -4,10 +4,10 @@
 
 
 {% with preferences|get_key:"pagination.placement" as paginator_placement %}
 {% with preferences|get_key:"pagination.placement" as paginator_placement %}
   {% if paginator_placement == 'top' or paginator_placement == 'both' %}
   {% 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 %}
   {% endif %}
   {% render_table table 'inc/table_htmx.html' %}
   {% render_table table 'inc/table_htmx.html' %}
   {% if paginator_placement != 'top' %}
   {% 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 %}
   {% endif %}
 {% endwith %}
 {% 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">
       <div class="btn-group btn-group-sm" role="group" aria-label="Pages">
         {% if page.has_previous %}
         {% if page.has_previous %}
           <a href="#"
           <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"
              class="btn btn-outline-secondary"
           >
           >
             <i class="mdi mdi-chevron-double-left"></i>
             <i class="mdi mdi-chevron-double-left"></i>
@@ -18,9 +18,9 @@
         {% for p in page.smart_pages %}
         {% for p in page.smart_pages %}
           {% if p %}
           {% if p %}
             <a href="#"
             <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 %}"
                class="btn btn-outline-secondary{% if page.number == p %} active{% endif %}"
             >
             >
               {{ p }}
               {{ p }}
@@ -33,9 +33,9 @@
         {% endfor %}
         {% endfor %}
         {% if page.has_next %}
         {% if page.has_next %}
           <a href="#"
           <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"
              class="btn btn-outline-secondary"
           >
           >
             <i class="mdi mdi-chevron-double-right"></i>
             <i class="mdi mdi-chevron-double-right"></i>
@@ -55,9 +55,9 @@
           {% for n in page.paginator.get_page_lengths %}
           {% for n in page.paginator.get_page_lengths %}
             <li>
             <li>
               <a href="#"
               <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"
                  class="dropdown-item"
               >{{ n }}</a>
               >{{ n }}</a>
             </li>
             </li>

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

@@ -1,16 +1,14 @@
 {% load render_table from django_tables2 %}
 {% 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 %}
     {% 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>
 </div>

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

@@ -1,59 +1,56 @@
 {% load django_tables2 %}
 {% 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>
         <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>
         </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 %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -75,17 +75,17 @@
     <div class="col col-md-12">
     <div class="col col-md-12">
       <div class="card">
       <div class="card">
         <h5 class="card-header">Sites</h5>
         <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>
       <div class="card">
       <div class="card">
         <h5 class="card-header">Providers</h5>
         <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>
       </div>
       {% plugin_full_width_page object %}
       {% plugin_full_width_page object %}
     </div>
     </div>

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

@@ -69,13 +69,10 @@
     <div class="col col-md-12">
     <div class="col col-md-12">
       <div class="card">
       <div class="card">
         <h5 class="card-header">Virtual IP Addresses</h5>
         <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 %}
         {% if perms.ipam.add_ipaddress %}
           <div class="card-footer text-end">
           <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">
             <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>
           </div>
         {% endif %}
         {% endif %}
       </div>
       </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 %}
       {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>

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

@@ -114,30 +114,9 @@
 	<div class="col col-md-8">
 	<div class="col col-md-8">
     {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
     {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
     {% if duplicate_ips_table.rows %}
     {% 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 %}
     {% 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' %}
     {% include 'inc/panels/services.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
 	</div>
 	</div>

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

@@ -16,7 +16,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -55,9 +55,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Terminations</h5>
       <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 %}
       {% if perms.ipam.add_l2vpntermination %}
         <div class="card-footer text-end noprint">
         <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 %}">
           <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 %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -16,7 +16,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -18,7 +18,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -35,16 +35,16 @@
           <tr>
           <tr>
             <th scope="row">Aggregates</th>
             <th scope="row">Aggregates</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
       </div>
       </div>
     </div>
     </div>
-    {% include 'inc/panels/tags.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
 	</div>
 	</div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
+    {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
   </div>
   </div>
@@ -53,10 +53,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Aggregates</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -35,40 +35,28 @@
           <tr>
           <tr>
             <th scope="row">Prefixes</th>
             <th scope="row">Prefixes</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th scope="row">IP Ranges</th>
             <th scope="row">IP Ranges</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
           <tr>
           <tr>
             <th scope="row">VLANs</th>
             <th scope="row">VLANs</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
       </div>
       </div>
     </div>
     </div>
-    {% include 'inc/panels/tags.html' %}
     {% plugin_left_page object %}
     {% plugin_left_page object %}
 	</div>
 	</div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
+    {% include 'inc/panels/tags.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% include 'inc/panels/custom_fields.html' %}
     {% plugin_right_page object %}
     {% plugin_right_page object %}
   </div>
   </div>
@@ -77,10 +65,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Prefixes</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -81,24 +81,22 @@
         </div>
         </div>
     </div>
     </div>
     <div class="row">
     <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>
             </div>
-            {% plugin_full_width_page object %}
+          {% endif %}
         </div>
         </div>
+        {% plugin_full_width_page object %}
+      </div>
     </div>
     </div>
 {% endblock %}
 {% endblock %}

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

@@ -7,7 +7,7 @@
   <form method="post">
   <form method="post">
     {% csrf_token %}
     {% csrf_token %}
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -7,7 +7,7 @@
   <form method="post">
   <form method="post">
     {% csrf_token %}
     {% csrf_token %}
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

+ 1 - 1
netbox/templates/search.html

@@ -30,7 +30,7 @@
   </div>
   </div>
   <div class="row px-3">
   <div class="row px-3">
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -34,7 +34,7 @@
             <tr>
             <tr>
               <th scope="row">Contacts</th>
               <th scope="row">Contacts</th>
               <td>
               <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>
               </td>
             </tr>
             </tr>
           </table>
           </table>
@@ -46,12 +46,11 @@
     <div class="col col-md-6">
     <div class="col col-md-6">
       {% include 'inc/panels/custom_fields.html' %}
       {% include 'inc/panels/custom_fields.html' %}
       <div class="card">
       <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 %}
         {% if perms.tenancy.add_contactgroup %}
           <div class="card-footer text-end noprint">
           <div class="card-footer text-end noprint">
             <a href="{% url 'tenancy:contactgroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
             <a href="{% url 'tenancy:contactgroup_add' %}?parent={{ object.pk }}" class="btn btn-sm btn-primary">
@@ -63,16 +62,14 @@
       {% plugin_right_page object %}
       {% plugin_right_page object %}
     </div>
     </div>
   </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>
     </div>
+    {% plugin_full_width_page object %}
   </div>
   </div>
 {% endblock %}
 {% endblock %}

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

@@ -42,7 +42,7 @@
           <tr>
           <tr>
             <th scope="row">Tenants</th>
             <th scope="row">Tenants</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -53,6 +53,20 @@
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/custom_fields.html' %}
     {% 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 %}
     {% plugin_right_page object %}
 	</div>
 	</div>
 </div>
 </div>
@@ -60,10 +74,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Tenants</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </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">
   <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
     {% csrf_token %}
     {% csrf_token %}
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -8,7 +8,7 @@
   <form method="post">
   <form method="post">
     {% csrf_token %}
     {% csrf_token %}
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -31,7 +31,7 @@
           <tr>
           <tr>
             <th scope="row">Clusters</th>
             <th scope="row">Clusters</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -50,10 +50,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Clusters</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -31,7 +31,7 @@
           <tr>
           <tr>
             <th scope="row">Clusters</th>
             <th scope="row">Clusters</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -49,10 +49,10 @@
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Clusters</h5>
       <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

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

@@ -9,7 +9,7 @@
     {% csrf_token %}
     {% csrf_token %}
 
 
     <div class="card">
     <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' %}
         {% include 'htmx/table.html' %}
       </div>
       </div>
     </div>
     </div>

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

@@ -80,16 +80,11 @@
 <div class="row mb-3">
 <div class="row mb-3">
     <div class="col col-md-12">
     <div class="col col-md-12">
         <div class="card">
         <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 %}
             {% if perms.ipam.add_ipaddress %}
                 <div class="card-footer text-end noprint">
                 <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">
                     <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>
           <tr>
             <th scope="row">Wireless LANs</th>
             <th scope="row">Wireless LANs</th>
             <td>
             <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>
             </td>
           </tr>
           </tr>
         </table>
         </table>
@@ -51,17 +51,31 @@
   </div>
   </div>
 	<div class="col col-md-6">
 	<div class="col col-md-6">
     {% include 'inc/panels/custom_fields.html' %}
     {% 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 %}
     {% plugin_right_page object %}
 	</div>
 	</div>
 </div>
 </div>
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <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>
     </div>
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>

+ 0 - 37
netbox/tenancy/views.py

@@ -34,17 +34,6 @@ class TenantGroupListView(generic.ObjectListView):
 class TenantGroupView(generic.ObjectView):
 class TenantGroupView(generic.ObjectView):
     queryset = TenantGroup.objects.all()
     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')
 @register_model_view(TenantGroup, 'edit')
 class TenantGroupEditView(generic.ObjectEditView):
 class TenantGroupEditView(generic.ObjectEditView):
@@ -182,32 +171,6 @@ class ContactGroupListView(generic.ObjectListView):
 class ContactGroupView(generic.ObjectView):
 class ContactGroupView(generic.ObjectView):
     queryset = ContactGroup.objects.all()
     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')
 @register_model_view(ContactGroup, 'edit')
 class ContactGroupEditView(generic.ObjectEditView):
 class ContactGroupEditView(generic.ObjectEditView):

+ 14 - 0
netbox/utilities/htmx.py

@@ -1,5 +1,19 @@
+from urllib.parse import urlparse
+
+
 def is_htmx(request):
 def is_htmx(request):
     """
     """
     Returns True if the request was made by HTMX; False otherwise.
     Returns True if the request was made by HTMX; False otherwise.
     """
     """
     return 'Hx-Request' in request.headers
     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):
 class ClusterGroupView(generic.ObjectView):
     queryset = ClusterGroup.objects.all()
     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')
 @register_model_view(ClusterGroup, 'edit')
 class ClusterGroupEditView(generic.ObjectEditView):
 class ClusterGroupEditView(generic.ObjectEditView):
@@ -444,11 +430,6 @@ class VMInterfaceView(generic.ObjectView):
     queryset = VMInterface.objects.all()
     queryset = VMInterface.objects.all()
 
 
     def get_extra_context(self, request, instance):
     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
         # Get child interfaces
         child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance)
         child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance)
@@ -473,7 +454,6 @@ class VMInterfaceView(generic.ObjectView):
         )
         )
 
 
         return {
         return {
-            'ipaddress_table': ipaddress_table,
             'child_interfaces_table': child_interfaces_tables,
             'child_interfaces_table': child_interfaces_tables,
             'vlan_table': vlan_table,
             'vlan_table': vlan_table,
         }
         }

+ 0 - 11
netbox/wireless/views.py

@@ -27,17 +27,6 @@ class WirelessLANGroupListView(generic.ObjectListView):
 class WirelessLANGroupView(generic.ObjectView):
 class WirelessLANGroupView(generic.ObjectView):
     queryset = WirelessLANGroup.objects.all()
     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')
 @register_model_view(WirelessLANGroup, 'edit')
 class WirelessLANGroupEditView(generic.ObjectEditView):
 class WirelessLANGroupEditView(generic.ObjectEditView):