Kaynağa Gözat

Merge branch 'develop' into feature

jeremystretch 4 yıl önce
ebeveyn
işleme
997e88af00
96 değiştirilmiş dosya ile 1240 ekleme ve 1042 silme
  1. 18 0
      docs/release-notes/version-3.1.md
  2. 42 36
      netbox/dcim/views.py
  3. 1 11
      netbox/extras/api/serializers.py
  4. 1 1
      netbox/extras/management/commands/nbshell.py
  5. 3 5
      netbox/extras/querysets.py
  6. 6 0
      netbox/ipam/models/ip.py
  7. 1 0
      netbox/ipam/urls.py
  8. 23 9
      netbox/ipam/utils.py
  9. 75 132
      netbox/ipam/views.py
  10. 76 0
      netbox/netbox/views/generic.py
  11. 0 0
      netbox/project-static/dist/netbox-dark.css
  12. 0 0
      netbox/project-static/dist/netbox-light.css
  13. 0 0
      netbox/project-static/dist/netbox-print.css
  14. 0 0
      netbox/project-static/dist/netbox.js
  15. 0 0
      netbox/project-static/dist/netbox.js.map
  16. 1 0
      netbox/project-static/package.json
  17. 0 2
      netbox/project-static/src/buttons/index.ts
  18. 0 14
      netbox/project-static/src/buttons/pagination.ts
  19. 1 0
      netbox/project-static/src/index.ts
  20. 2 104
      netbox/project-static/src/search.ts
  21. 0 12
      netbox/project-static/styles/netbox.scss
  22. 5 0
      netbox/project-static/yarn.lock
  23. 15 15
      netbox/templates/circuits/circuittype.html
  24. 20 22
      netbox/templates/circuits/provider.html
  25. 11 16
      netbox/templates/circuits/providernetwork.html
  26. 4 9
      netbox/templates/dcim/connections_list.html
  27. 18 18
      netbox/templates/dcim/device.html
  28. 8 3
      netbox/templates/dcim/device/consoleports.html
  29. 8 3
      netbox/templates/dcim/device/consoleserverports.html
  30. 8 3
      netbox/templates/dcim/device/devicebays.html
  31. 8 3
      netbox/templates/dcim/device/frontports.html
  32. 16 3
      netbox/templates/dcim/device/interfaces.html
  33. 8 3
      netbox/templates/dcim/device/inventory.html
  34. 3 3
      netbox/templates/dcim/device/lldp_neighbors.html
  35. 8 3
      netbox/templates/dcim/device/poweroutlets.html
  36. 8 3
      netbox/templates/dcim/device/powerports.html
  37. 8 3
      netbox/templates/dcim/device/rearports.html
  38. 13 13
      netbox/templates/dcim/devicerole.html
  39. 7 11
      netbox/templates/dcim/devicetype/component_templates.html
  40. 1 1
      netbox/templates/dcim/interface.html
  41. 15 15
      netbox/templates/dcim/location.html
  42. 13 13
      netbox/templates/dcim/manufacturer.html
  43. 15 15
      netbox/templates/dcim/platform.html
  44. 1 1
      netbox/templates/dcim/powerpanel.html
  45. 13 13
      netbox/templates/dcim/rackrole.html
  46. 18 18
      netbox/templates/dcim/region.html
  47. 15 15
      netbox/templates/dcim/sitegroup.html
  48. 11 3
      netbox/templates/extras/object_changelog.html
  49. 6 2
      netbox/templates/extras/object_journal.html
  50. 11 4
      netbox/templates/extras/tag.html
  51. 3 2
      netbox/templates/generic/object_bulk_add_component.html
  52. 4 1
      netbox/templates/generic/object_bulk_delete.html
  53. 4 1
      netbox/templates/generic/object_bulk_edit.html
  54. 4 1
      netbox/templates/generic/object_bulk_remove.html
  55. 3 7
      netbox/templates/generic/object_list.html
  56. 5 0
      netbox/templates/htmx/table.html
  57. 41 40
      netbox/templates/inc/paginator.html
  58. 72 0
      netbox/templates/inc/paginator_htmx.html
  59. 4 4
      netbox/templates/inc/panel_table.html
  60. 37 39
      netbox/templates/inc/table.html
  61. 8 3
      netbox/templates/inc/table_controls_htmx.html
  62. 49 0
      netbox/templates/inc/table_htmx.html
  63. 53 69
      netbox/templates/ipam/aggregate.html
  64. 23 0
      netbox/templates/ipam/aggregate/base.html
  65. 36 0
      netbox/templates/ipam/aggregate/prefixes.html
  66. 4 3
      netbox/templates/ipam/asn.html
  67. 2 2
      netbox/templates/ipam/fhrpgroup.html
  68. 8 5
      netbox/templates/ipam/inc/toggle_available.html
  69. 4 4
      netbox/templates/ipam/ipaddress.html
  70. 4 1
      netbox/templates/ipam/ipaddress_assign.html
  71. 26 4
      netbox/templates/ipam/iprange/ip_addresses.html
  72. 25 7
      netbox/templates/ipam/prefix/ip_addresses.html
  73. 25 7
      netbox/templates/ipam/prefix/ip_ranges.html
  74. 25 7
      netbox/templates/ipam/prefix/prefixes.html
  75. 13 13
      netbox/templates/ipam/rir.html
  76. 13 13
      netbox/templates/ipam/role.html
  77. 12 4
      netbox/templates/ipam/vlan/interfaces.html
  78. 12 4
      netbox/templates/ipam/vlan/vminterfaces.html
  79. 6 17
      netbox/templates/ipam/vlangroup.html
  80. 4 8
      netbox/templates/tenancy/contact.html
  81. 9 17
      netbox/templates/tenancy/contactgroup.html
  82. 4 3
      netbox/templates/tenancy/contactrole.html
  83. 15 15
      netbox/templates/tenancy/tenantgroup.html
  84. 0 62
      netbox/templates/utilities/obj_table.html
  85. 15 16
      netbox/templates/virtualization/cluster/devices.html
  86. 23 9
      netbox/templates/virtualization/cluster/virtual_machines.html
  87. 13 13
      netbox/templates/virtualization/clustergroup.html
  88. 13 13
      netbox/templates/virtualization/clustertype.html
  89. 18 18
      netbox/templates/virtualization/virtualmachine.html
  90. 9 4
      netbox/templates/virtualization/virtualmachine/interfaces.html
  91. 1 1
      netbox/templates/virtualization/vminterface.html
  92. 4 3
      netbox/templates/wireless/wirelesslan.html
  93. 14 12
      netbox/templates/wireless/wirelesslangroup.html
  94. 5 0
      netbox/utilities/htmx.py
  95. 1 0
      netbox/virtualization/tables.py
  96. 22 30
      netbox/virtualization/views.py

+ 18 - 0
docs/release-notes/version-3.1.md

@@ -1,5 +1,23 @@
 # NetBox v3.1
 # NetBox v3.1
 
 
+## v3.1.2 (FUTURE)
+
+### Enhancements
+
+* [#7665](https://github.com/netbox-community/netbox/issues/7665) - Add toggle to show only available child prefixes
+* [#8057](https://github.com/netbox-community/netbox/issues/8057) - Dynamic object tables using HTMX
+* [#8080](https://github.com/netbox-community/netbox/issues/8080) - Link to NAT IPs for device/VM primary IPs
+
+### Bug Fixes
+
+* [#7674](https://github.com/netbox-community/netbox/issues/7674) - Fix inadvertent application of device type context to virtual machines
+* [#8074](https://github.com/netbox-community/netbox/issues/8074) - Ordering VMs by name should reference naturalized value
+* [#8077](https://github.com/netbox-community/netbox/issues/8077) - Fix exception when attaching image to location, circuit, or power panel
+* [#8078](https://github.com/netbox-community/netbox/issues/8078) - Add missing wireless models to `lsmodels()` in `nbshell`
+* [#8079](https://github.com/netbox-community/netbox/issues/8079) - Fix validation of LLDP neighbors when connected device has an asset tag
+
+---
+
 ## v3.1.1 (2021-12-13)
 ## v3.1.1 (2021-12-13)
 
 
 ### Enhancements
 ### Enhancements

+ 42 - 36
netbox/dcim/views.py

@@ -36,26 +36,15 @@ from .models import (
 )
 )
 
 
 
 
-class DeviceComponentsView(generic.ObjectView):
+class DeviceComponentsView(generic.ObjectChildrenView):
     queryset = Device.objects.all()
     queryset = Device.objects.all()
-    model = None
-    table = None
 
 
-    def get_components(self, request, instance):
-        return self.model.objects.restrict(request.user, 'view').filter(device=instance)
+    def get_children(self, request, parent):
+        return self.child_model.objects.restrict(request.user, 'view').filter(device=parent)
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
-        components = self.get_components(request, instance)
-        table = self.table(data=components, user=request.user)
-        change_perm = f'{self.model._meta.app_label}.change_{self.model._meta.model_name}'
-        delete_perm = f'{self.model._meta.app_label}.delete_{self.model._meta.model_name}'
-        if request.user.has_perm(change_perm) or request.user.has_perm(delete_perm):
-            table.columns.show('pk')
-        paginate_table(table, request)
-
         return {
         return {
-            'table': table,
-            'active_tab': f"{self.model._meta.verbose_name_plural.replace(' ', '-')}",
+            'active_tab': f"{self.child_model._meta.verbose_name_plural.replace(' ', '-')}",
         }
         }
 
 
 
 
@@ -63,8 +52,8 @@ class DeviceTypeComponentsView(DeviceComponentsView):
     queryset = DeviceType.objects.all()
     queryset = DeviceType.objects.all()
     template_name = 'dcim/devicetype/component_templates.html'
     template_name = 'dcim/devicetype/component_templates.html'
 
 
-    def get_components(self, request, instance):
-        return self.model.objects.restrict(request.user, 'view').filter(device_type=instance)
+    def get_children(self, request, parent):
+        return self.child_model.objects.restrict(request.user, 'view').filter(device_type=parent)
 
 
 
 
 class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
 class BulkDisconnectView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
@@ -806,43 +795,51 @@ class DeviceTypeView(generic.ObjectView):
 
 
 
 
 class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
 class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
-    model = ConsolePortTemplate
+    child_model = ConsolePortTemplate
     table = tables.ConsolePortTemplateTable
     table = tables.ConsolePortTemplateTable
+    filterset = filtersets.ConsolePortTemplateFilterSet
 
 
 
 
 class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
 class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
-    model = ConsoleServerPortTemplate
+    child_model = ConsoleServerPortTemplate
     table = tables.ConsoleServerPortTemplateTable
     table = tables.ConsoleServerPortTemplateTable
+    filterset = filtersets.ConsoleServerPortTemplateFilterSet
 
 
 
 
 class DeviceTypePowerPortsView(DeviceTypeComponentsView):
 class DeviceTypePowerPortsView(DeviceTypeComponentsView):
-    model = PowerPortTemplate
+    child_model = PowerPortTemplate
     table = tables.PowerPortTemplateTable
     table = tables.PowerPortTemplateTable
+    filterset = filtersets.PowerPortTemplateFilterSet
 
 
 
 
 class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
 class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
-    model = PowerOutletTemplate
+    child_model = PowerOutletTemplate
     table = tables.PowerOutletTemplateTable
     table = tables.PowerOutletTemplateTable
+    filterset = filtersets.PowerOutletTemplateFilterSet
 
 
 
 
 class DeviceTypeInterfacesView(DeviceTypeComponentsView):
 class DeviceTypeInterfacesView(DeviceTypeComponentsView):
-    model = InterfaceTemplate
+    child_model = InterfaceTemplate
     table = tables.InterfaceTemplateTable
     table = tables.InterfaceTemplateTable
+    filterset = filtersets.InterfaceTemplateFilterSet
 
 
 
 
 class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
 class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
-    model = FrontPortTemplate
+    child_model = FrontPortTemplate
     table = tables.FrontPortTemplateTable
     table = tables.FrontPortTemplateTable
+    filterset = filtersets.FrontPortTemplateFilterSet
 
 
 
 
 class DeviceTypeRearPortsView(DeviceTypeComponentsView):
 class DeviceTypeRearPortsView(DeviceTypeComponentsView):
-    model = RearPortTemplate
+    child_model = RearPortTemplate
     table = tables.RearPortTemplateTable
     table = tables.RearPortTemplateTable
+    filterset = filtersets.RearPortTemplateFilterSet
 
 
 
 
 class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
 class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
-    model = DeviceBayTemplate
+    child_model = DeviceBayTemplate
     table = tables.DeviceBayTemplateTable
     table = tables.DeviceBayTemplateTable
+    filterset = filtersets.DeviceBayTemplateFilterSet
 
 
 
 
 class DeviceTypeEditView(generic.ObjectEditView):
 class DeviceTypeEditView(generic.ObjectEditView):
@@ -1337,62 +1334,71 @@ class DeviceView(generic.ObjectView):
 
 
 
 
 class DeviceConsolePortsView(DeviceComponentsView):
 class DeviceConsolePortsView(DeviceComponentsView):
-    model = ConsolePort
+    child_model = ConsolePort
     table = tables.DeviceConsolePortTable
     table = tables.DeviceConsolePortTable
+    filterset = filtersets.ConsolePortFilterSet
     template_name = 'dcim/device/consoleports.html'
     template_name = 'dcim/device/consoleports.html'
 
 
 
 
 class DeviceConsoleServerPortsView(DeviceComponentsView):
 class DeviceConsoleServerPortsView(DeviceComponentsView):
-    model = ConsoleServerPort
+    child_model = ConsoleServerPort
     table = tables.DeviceConsoleServerPortTable
     table = tables.DeviceConsoleServerPortTable
+    filterset = filtersets.ConsoleServerPortFilterSet
     template_name = 'dcim/device/consoleserverports.html'
     template_name = 'dcim/device/consoleserverports.html'
 
 
 
 
 class DevicePowerPortsView(DeviceComponentsView):
 class DevicePowerPortsView(DeviceComponentsView):
-    model = PowerPort
+    child_model = PowerPort
     table = tables.DevicePowerPortTable
     table = tables.DevicePowerPortTable
+    filterset = filtersets.PowerPortFilterSet
     template_name = 'dcim/device/powerports.html'
     template_name = 'dcim/device/powerports.html'
 
 
 
 
 class DevicePowerOutletsView(DeviceComponentsView):
 class DevicePowerOutletsView(DeviceComponentsView):
-    model = PowerOutlet
+    child_model = PowerOutlet
     table = tables.DevicePowerOutletTable
     table = tables.DevicePowerOutletTable
+    filterset = filtersets.PowerOutletFilterSet
     template_name = 'dcim/device/poweroutlets.html'
     template_name = 'dcim/device/poweroutlets.html'
 
 
 
 
 class DeviceInterfacesView(DeviceComponentsView):
 class DeviceInterfacesView(DeviceComponentsView):
-    model = Interface
+    child_model = Interface
     table = tables.DeviceInterfaceTable
     table = tables.DeviceInterfaceTable
+    filterset = filtersets.InterfaceFilterSet
     template_name = 'dcim/device/interfaces.html'
     template_name = 'dcim/device/interfaces.html'
 
 
-    def get_components(self, request, instance):
-        return instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
+    def get_children(self, request, parent):
+        return parent.vc_interfaces().restrict(request.user, 'view').prefetch_related(
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user))
             Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user))
         )
         )
 
 
 
 
 class DeviceFrontPortsView(DeviceComponentsView):
 class DeviceFrontPortsView(DeviceComponentsView):
-    model = FrontPort
+    child_model = FrontPort
     table = tables.DeviceFrontPortTable
     table = tables.DeviceFrontPortTable
+    filterset = filtersets.FrontPortFilterSet
     template_name = 'dcim/device/frontports.html'
     template_name = 'dcim/device/frontports.html'
 
 
 
 
 class DeviceRearPortsView(DeviceComponentsView):
 class DeviceRearPortsView(DeviceComponentsView):
-    model = RearPort
+    child_model = RearPort
     table = tables.DeviceRearPortTable
     table = tables.DeviceRearPortTable
+    filterset = filtersets.RearPortFilterSet
     template_name = 'dcim/device/rearports.html'
     template_name = 'dcim/device/rearports.html'
 
 
 
 
 class DeviceDeviceBaysView(DeviceComponentsView):
 class DeviceDeviceBaysView(DeviceComponentsView):
-    model = DeviceBay
+    child_model = DeviceBay
     table = tables.DeviceDeviceBayTable
     table = tables.DeviceDeviceBayTable
+    filterset = filtersets.DeviceBayFilterSet
     template_name = 'dcim/device/devicebays.html'
     template_name = 'dcim/device/devicebays.html'
 
 
 
 
 class DeviceInventoryView(DeviceComponentsView):
 class DeviceInventoryView(DeviceComponentsView):
-    model = InventoryItem
+    child_model = InventoryItem
     table = tables.DeviceInventoryItemTable
     table = tables.DeviceInventoryItemTable
+    filterset = filtersets.InventoryItemFilterSet
     template_name = 'dcim/device/inventory.html'
     template_name = 'dcim/device/inventory.html'
 
 
 
 

+ 1 - 11
netbox/extras/api/serializers.py

@@ -170,17 +170,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
 
 
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     @swagger_serializer_method(serializer_or_field=serializers.DictField)
     def get_parent(self, obj):
     def get_parent(self, obj):
-
-        # Static mapping of models to their nested serializers
-        if isinstance(obj.parent, Device):
-            serializer = NestedDeviceSerializer
-        elif isinstance(obj.parent, Rack):
-            serializer = NestedRackSerializer
-        elif isinstance(obj.parent, Site):
-            serializer = NestedSiteSerializer
-        else:
-            raise Exception("Unexpected type of parent object for ImageAttachment")
-
+        serializer = get_serializer_for_model(obj.parent, prefix='Nested')
         return serializer(obj.parent, context={'request': self.context['request']}).data
         return serializer(obj.parent, context={'request': self.context['request']}).data
 
 
 
 

+ 1 - 1
netbox/extras/management/commands/nbshell.py

@@ -9,7 +9,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 
 
-APPS = ['circuits', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization']
+APPS = ('circuits', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'wireless')
 
 
 BANNER_TEXT = """### NetBox interactive shell ({node})
 BANNER_TEXT = """### NetBox interactive shell ({node})
 ### Python {python} | Django {django} | NetBox {netbox}
 ### Python {python} | Django {django} | NetBox {netbox}

+ 3 - 5
netbox/extras/querysets.py

@@ -22,7 +22,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
         # Device type assignment is relevant only for Devices
         # Device type assignment is relevant only for Devices
         device_type = getattr(obj, 'device_type', None)
         device_type = getattr(obj, 'device_type', None)
 
 
-        # Cluster assignment is relevant only for VirtualMachines
+        # Get assigned Cluster and ClusterGroup, if any
         cluster = getattr(obj, 'cluster', None)
         cluster = getattr(obj, 'cluster', None)
         cluster_group = getattr(cluster, 'group', None)
         cluster_group = getattr(cluster, 'group', None)
 
 
@@ -67,11 +67,8 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
     Includes a method which appends an annotation of aggregated config context JSON data objects. This is
     Includes a method which appends an annotation of aggregated config context JSON data objects. This is
     implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
     implemented as a subquery which performs all the joins necessary to filter relevant config context objects.
     This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
     This offers a substantial performance gain over ConfigContextQuerySet.get_for_object() when dealing with
-    multiple objects.
-
-    This allows the annotation to be entirely optional.
+    multiple objects. This allows the annotation to be entirely optional.
     """
     """
-
     def annotate_config_context_data(self):
     def annotate_config_context_data(self):
         """
         """
         Attach the subquery annotation to the base queryset
         Attach the subquery annotation to the base queryset
@@ -123,6 +120,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
         elif self.model._meta.model_name == 'virtualmachine':
         elif self.model._meta.model_name == 'virtualmachine':
             base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
             base_query.add((Q(roles=OuterRef('role')) | Q(roles=None)), Q.AND)
             base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
             base_query.add((Q(sites=OuterRef('cluster__site')) | Q(sites=None)), Q.AND)
+            base_query.add(Q(device_types=None), Q.AND)
             region_field = 'cluster__site__region'
             region_field = 'cluster__site__region'
             sitegroup_field = 'cluster__site__group'
             sitegroup_field = 'cluster__site__group'
 
 

+ 6 - 0
netbox/ipam/models/ip.py

@@ -195,6 +195,12 @@ class Aggregate(PrimaryModel):
             return self.prefix.version
             return self.prefix.version
         return None
         return None
 
 
+    def get_child_prefixes(self):
+        """
+        Return all Prefixes within this Aggregate
+        """
+        return Prefix.objects.filter(prefix__net_contained=str(self.prefix))
+
     def get_utilization(self):
     def get_utilization(self):
         """
         """
         Determine the prefix utilization of the aggregate and return it as a percentage.
         Determine the prefix utilization of the aggregate and return it as a percentage.

+ 1 - 0
netbox/ipam/urls.py

@@ -61,6 +61,7 @@ urlpatterns = [
     path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
     path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'),
     path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
     path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'),
     path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
     path('aggregates/<int:pk>/', views.AggregateView.as_view(), name='aggregate'),
+    path('aggregates/<int:pk>/prefixes/', views.AggregatePrefixesView.as_view(), name='aggregate_prefixes'),
     path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
     path('aggregates/<int:pk>/edit/', views.AggregateEditView.as_view(), name='aggregate_edit'),
     path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
     path('aggregates/<int:pk>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
     path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),
     path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),

+ 23 - 9
netbox/ipam/utils.py

@@ -4,20 +4,34 @@ from .constants import *
 from .models import Prefix, VLAN
 from .models import Prefix, VLAN
 
 
 
 
-def add_available_prefixes(parent, prefix_list):
+def add_requested_prefixes(parent, prefix_list, show_available=True, show_assigned=True):
     """
     """
-    Create fake Prefix objects for all unallocated space within a prefix.
+    Return a list of requested prefixes using show_available, show_assigned filters. If available prefixes are
+    requested, create fake Prefix objects for all unallocated space within a prefix.
+
+    :param parent: Parent Prefix instance
+    :param prefix_list: Child prefixes list
+    :param show_available: Include available prefixes.
+    :param show_assigned: Show assigned prefixes.
     """
     """
+    child_prefixes = []
+
+    # Add available prefixes to the table if requested
+    if prefix_list and show_available:
+
+        # Find all unallocated space, add fake Prefix objects to child_prefixes.
+        available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
+        available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
+        child_prefixes = child_prefixes + available_prefixes
 
 
-    # Find all unallocated space
-    available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
-    available_prefixes = [Prefix(prefix=p, status=None) for p in available_prefixes.iter_cidrs()]
+    # Add assigned prefixes to the table if requested
+    if prefix_list and show_assigned:
+        child_prefixes = child_prefixes + list(prefix_list)
 
 
-    # Concatenate and sort complete list of children
-    prefix_list = list(prefix_list) + available_prefixes
-    prefix_list.sort(key=lambda p: p.prefix)
+    # Sort child prefixes after additions
+    child_prefixes.sort(key=lambda p: p.prefix)
 
 
-    return prefix_list
+    return child_prefixes
 
 
 
 
 def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
 def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):

+ 75 - 132
netbox/ipam/views.py

@@ -1,21 +1,22 @@
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db.models import Prefetch
 from django.db.models import Prefetch
 from django.db.models.expressions import RawSQL
 from django.db.models.expressions import RawSQL
-from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 
 
+from dcim.filtersets import InterfaceFilterSet
 from dcim.models import Device, Interface, Site
 from dcim.models import Device, Interface, Site
 from dcim.tables import SiteTable
 from dcim.tables import SiteTable
 from netbox.views import generic
 from netbox.views import generic
 from utilities.tables import paginate_table
 from utilities.tables import paginate_table
 from utilities.utils import count_related
 from utilities.utils import count_related
+from virtualization.filtersets import VMInterfaceFilterSet
 from virtualization.models import VirtualMachine, VMInterface
 from virtualization.models import VirtualMachine, VMInterface
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables
 from .constants import *
 from .constants import *
 from .models import *
 from .models import *
 from .models import ASN
 from .models import ASN
-from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
+from .utils import add_requested_prefixes, add_available_vlans
 
 
 
 
 #
 #
@@ -274,37 +275,32 @@ class AggregateListView(generic.ObjectListView):
 class AggregateView(generic.ObjectView):
 class AggregateView(generic.ObjectView):
     queryset = Aggregate.objects.all()
     queryset = Aggregate.objects.all()
 
 
-    def get_extra_context(self, request, instance):
-        # Find all child prefixes contained by this aggregate
-        child_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
-            prefix__net_contained_or_equal=str(instance.prefix)
-        ).prefetch_related(
-            'site', 'role'
-        ).order_by(
-            'prefix'
-        )
 
 
-        # Add available prefixes to the table if requested
-        if request.GET.get('show_available', 'true') == 'true':
-            child_prefixes = add_available_prefixes(instance.prefix, child_prefixes)
+class AggregatePrefixesView(generic.ObjectChildrenView):
+    queryset = Aggregate.objects.all()
+    child_model = Prefix
+    table = tables.PrefixTable
+    filterset = filtersets.PrefixFilterSet
+    template_name = 'ipam/aggregate/prefixes.html'
 
 
-        prefix_table = tables.PrefixTable(child_prefixes, exclude=('utilization',))
-        if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
-            prefix_table.columns.show('pk')
-        paginate_table(prefix_table, request)
+    def get_children(self, request, parent):
+        return Prefix.objects.restrict(request.user, 'view').filter(
+            prefix__net_contained_or_equal=str(parent.prefix)
+        ).prefetch_related('site', 'role', 'tenant', 'vlan')
 
 
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'add': request.user.has_perm('ipam.add_prefix'),
-            'change': request.user.has_perm('ipam.change_prefix'),
-            'delete': request.user.has_perm('ipam.delete_prefix'),
-        }
+    def prep_table_data(self, request, queryset, parent):
+        # Determine whether to show assigned prefixes, available prefixes, or both
+        show_available = bool(request.GET.get('show_available', 'true') == 'true')
+        show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')
+
+        return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'prefix_table': prefix_table,
-            'permissions': permissions,
             'bulk_querystring': f'within={instance.prefix}',
             'bulk_querystring': f'within={instance.prefix}',
-            'show_available': request.GET.get('show_available', 'true') == 'true',
+            'active_tab': 'prefixes',
+            'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
+            'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
         }
         }
 
 
 
 
@@ -451,104 +447,65 @@ class PrefixView(generic.ObjectView):
         }
         }
 
 
 
 
-class PrefixPrefixesView(generic.ObjectView):
+class PrefixPrefixesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
+    child_model = Prefix
+    table = tables.PrefixTable
+    filterset = filtersets.PrefixFilterSet
     template_name = 'ipam/prefix/prefixes.html'
     template_name = 'ipam/prefix/prefixes.html'
 
 
-    def get_extra_context(self, request, instance):
-        # Child prefixes table
-        child_prefixes = instance.get_child_prefixes().restrict(request.user, 'view').prefetch_related(
-            'site', 'vlan', 'role',
-        )
-
-        # Add available prefixes to the table if requested
-        if child_prefixes and request.GET.get('show_available', 'true') == 'true':
-            child_prefixes = add_available_prefixes(instance.prefix, child_prefixes)
+    def get_children(self, request, parent):
+        return parent.get_child_prefixes().restrict(request.user, 'view')
 
 
-        table = tables.PrefixTable(child_prefixes, user=request.user, exclude=('utilization',))
-        if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
-            table.columns.show('pk')
-        paginate_table(table, request)
+    def prep_table_data(self, request, queryset, parent):
+        # Determine whether to show assigned prefixes, available prefixes, or both
+        show_available = bool(request.GET.get('show_available', 'true') == 'true')
+        show_assigned = bool(request.GET.get('show_assigned', 'true') == 'true')
 
 
-        bulk_querystring = 'vrf_id={}&within={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'change': request.user.has_perm('ipam.change_prefix'),
-            'delete': request.user.has_perm('ipam.delete_prefix'),
-        }
+        return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'table': table,
-            'permissions': permissions,
-            'bulk_querystring': bulk_querystring,
+            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&within={instance.prefix}",
             'active_tab': 'prefixes',
             'active_tab': 'prefixes',
             'first_available_prefix': instance.get_first_available_prefix(),
             'first_available_prefix': instance.get_first_available_prefix(),
-            'show_available': request.GET.get('show_available', 'true') == 'true',
+            'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
+            'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
         }
         }
 
 
 
 
-class PrefixIPRangesView(generic.ObjectView):
+class PrefixIPRangesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
+    child_model = IPRange
+    table = tables.IPRangeTable
+    filterset = filtersets.IPRangeFilterSet
     template_name = 'ipam/prefix/ip_ranges.html'
     template_name = 'ipam/prefix/ip_ranges.html'
 
 
-    def get_extra_context(self, request, instance):
-        # Find all IPRanges belonging to this Prefix
-        ip_ranges = instance.get_child_ranges().restrict(request.user, 'view').prefetch_related('vrf')
-
-        table = tables.IPRangeTable(ip_ranges, user=request.user)
-        if request.user.has_perm('ipam.change_iprange') or request.user.has_perm('ipam.delete_iprange'):
-            table.columns.show('pk')
-        paginate_table(table, request)
-
-        bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'change': request.user.has_perm('ipam.change_iprange'),
-            'delete': request.user.has_perm('ipam.delete_iprange'),
-        }
+    def get_children(self, request, parent):
+        return parent.get_child_ranges().restrict(request.user, 'view')
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'table': table,
-            'permissions': permissions,
-            'bulk_querystring': bulk_querystring,
+            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
             'active_tab': 'ip-ranges',
             'active_tab': 'ip-ranges',
         }
         }
 
 
 
 
-class PrefixIPAddressesView(generic.ObjectView):
+class PrefixIPAddressesView(generic.ObjectChildrenView):
     queryset = Prefix.objects.all()
     queryset = Prefix.objects.all()
+    child_model = IPAddress
+    table = tables.IPAddressTable
+    filterset = filtersets.IPAddressFilterSet
     template_name = 'ipam/prefix/ip_addresses.html'
     template_name = 'ipam/prefix/ip_addresses.html'
 
 
-    def get_extra_context(self, request, instance):
-        # Find all IPAddresses belonging to this Prefix
-        ipaddresses = instance.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf')
-
-        # Add available IP addresses to the table if requested
-        if request.GET.get('show_available', 'true') == 'true':
-            ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool)
-
-        table = tables.IPAddressTable(ipaddresses, user=request.user)
-        if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
-            table.columns.show('pk')
-        paginate_table(table, request)
-
-        bulk_querystring = 'vrf_id={}&parent={}'.format(instance.vrf.pk if instance.vrf else '0', instance.prefix)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'change': request.user.has_perm('ipam.change_ipaddress'),
-            'delete': request.user.has_perm('ipam.delete_ipaddress'),
-        }
+    def get_children(self, request, parent):
+        return parent.get_child_ips().restrict(request.user, 'view')
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'table': table,
-            'permissions': permissions,
-            'bulk_querystring': bulk_querystring,
+            'bulk_querystring': f"vrf_id={instance.vrf.pk if instance.vrf else '0'}&parent={instance.prefix}",
             'active_tab': 'ip-addresses',
             'active_tab': 'ip-addresses',
             'first_available_ip': instance.get_first_available_ip(),
             'first_available_ip': instance.get_first_available_ip(),
-            'show_available': request.GET.get('show_available', 'true') == 'true',
         }
         }
 
 
 
 
@@ -596,35 +553,19 @@ class IPRangeView(generic.ObjectView):
     queryset = IPRange.objects.all()
     queryset = IPRange.objects.all()
 
 
 
 
-class IPRangeIPAddressesView(generic.ObjectView):
+class IPRangeIPAddressesView(generic.ObjectChildrenView):
     queryset = IPRange.objects.all()
     queryset = IPRange.objects.all()
+    child_model = IPAddress
+    table = tables.IPAddressTable
+    filterset = filtersets.IPAddressFilterSet
     template_name = 'ipam/iprange/ip_addresses.html'
     template_name = 'ipam/iprange/ip_addresses.html'
 
 
-    def get_extra_context(self, request, instance):
-        # Find all IPAddresses within this range
-        ipaddresses = instance.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf')
-
-        # Add available IP addresses to the table if requested
-        # if request.GET.get('show_available', 'true') == 'true':
-        #     ipaddresses = add_available_ipaddresses(instance.prefix, ipaddresses, instance.is_pool)
-
-        ip_table = tables.IPAddressTable(ipaddresses)
-        if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
-            ip_table.columns.show('pk')
-        paginate_table(ip_table, request)
-
-        # Compile permissions list for rendering the object table
-        permissions = {
-            'add': request.user.has_perm('ipam.add_ipaddress'),
-            'change': request.user.has_perm('ipam.change_ipaddress'),
-            'delete': request.user.has_perm('ipam.delete_ipaddress'),
-        }
+    def get_children(self, request, parent):
+        return parent.get_child_ips().restrict(request.user, 'view')
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'ip_table': ip_table,
-            'permissions': permissions,
             'active_tab': 'ip-addresses',
             'active_tab': 'ip-addresses',
-            'show_available': request.GET.get('show_available', 'true') == 'true',
         }
         }
 
 
 
 
@@ -1012,32 +953,34 @@ class VLANView(generic.ObjectView):
         }
         }
 
 
 
 
-class VLANInterfacesView(generic.ObjectView):
+class VLANInterfacesView(generic.ObjectChildrenView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
+    child_model = Interface
+    table = tables.VLANDevicesTable
+    filterset = InterfaceFilterSet
     template_name = 'ipam/vlan/interfaces.html'
     template_name = 'ipam/vlan/interfaces.html'
 
 
-    def get_extra_context(self, request, instance):
-        interfaces = instance.get_interfaces().prefetch_related('device')
-        members_table = tables.VLANDevicesTable(interfaces)
-        paginate_table(members_table, request)
+    def get_children(self, request, parent):
+        return parent.get_interfaces().restrict(request.user, 'view')
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'members_table': members_table,
             'active_tab': 'interfaces',
             'active_tab': 'interfaces',
         }
         }
 
 
 
 
-class VLANVMInterfacesView(generic.ObjectView):
+class VLANVMInterfacesView(generic.ObjectChildrenView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
+    child_model = VMInterface
+    table = tables.VLANVirtualMachinesTable
+    filterset = VMInterfaceFilterSet
     template_name = 'ipam/vlan/vminterfaces.html'
     template_name = 'ipam/vlan/vminterfaces.html'
 
 
-    def get_extra_context(self, request, instance):
-        interfaces = instance.get_vminterfaces().prefetch_related('virtual_machine')
-        members_table = tables.VLANVirtualMachinesTable(interfaces)
-        paginate_table(members_table, request)
+    def get_children(self, request, parent):
+        return parent.get_vminterfaces().restrict(request.user, 'view')
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'members_table': members_table,
             'active_tab': 'vminterfaces',
             'active_tab': 'vminterfaces',
         }
         }
 
 

+ 76 - 0
netbox/netbox/views/generic.py

@@ -23,6 +23,7 @@ from utilities.exceptions import AbortTransaction, PermissionsViolation
 from utilities.forms import (
 from utilities.forms import (
     BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, ImportForm, restrict_form_fields,
     BootstrapMixin, BulkRenameForm, ConfirmationForm, CSVDataField, CSVFileField, ImportForm, restrict_form_fields,
 )
 )
+from utilities.htmx import is_htmx
 from utilities.permissions import get_permission_for_model
 from utilities.permissions import get_permission_for_model
 from utilities.tables import paginate_table
 from utilities.tables import paginate_table
 from utilities.utils import normalize_querydict, prepare_cloned_fields
 from utilities.utils import normalize_querydict, prepare_cloned_fields
@@ -74,6 +75,75 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
         })
         })
 
 
 
 
+class ObjectChildrenView(ObjectView):
+    """
+    Display a table of child objects associated with the parent object.
+
+    queryset: The base queryset for retrieving the *parent* object
+    table: Table class used to render child objects list
+    template_name: Name of the template to use
+    """
+    queryset = None
+    child_model = None
+    table = None
+    filterset = None
+    template_name = None
+
+    def get_children(self, request, parent):
+        """
+        Return a QuerySet of child objects.
+
+        request: The current request
+        parent: The parent object
+        """
+        raise NotImplementedError(f'{self.__class__.__name__} must implement get_children()')
+
+    def prep_table_data(self, request, queryset, parent):
+        """
+        Provides a hook for subclassed views to modify data before initializing the table.
+
+        :param request: The current request
+        :param queryset: The filtered queryset of child objects
+        :param parent: The parent object
+        """
+        return queryset
+
+    def get(self, request, *args, **kwargs):
+        """
+        GET handler for rendering child objects.
+        """
+        instance = get_object_or_404(self.queryset, **kwargs)
+        child_objects = self.get_children(request, instance)
+
+        if self.filterset:
+            child_objects = self.filterset(request.GET, child_objects).qs
+
+        permissions = {}
+        for action in ('change', 'delete'):
+            perm_name = get_permission_for_model(self.child_model, action)
+            permissions[action] = request.user.has_perm(perm_name)
+
+        table = self.table(self.prep_table_data(request, child_objects, instance), user=request.user)
+        # Determine whether to display bulk action checkboxes
+        if 'pk' in table.base_columns and (permissions['change'] or permissions['delete']):
+            table.columns.show('pk')
+        paginate_table(table, request)
+
+        # If this is an HTMX request, return only the rendered table HTML
+        if is_htmx(request):
+            return render(request, 'htmx/table.html', {
+                'object': instance,
+                'table': table,
+            })
+
+        return render(request, self.get_template_name(), {
+            'object': instance,
+            'table': table,
+            'permissions': permissions,
+            **self.get_extra_context(request, instance),
+        })
+
+
 class ObjectListView(ObjectPermissionRequiredMixin, View):
 class ObjectListView(ObjectPermissionRequiredMixin, View):
     """
     """
     List a series of objects.
     List a series of objects.
@@ -208,6 +278,12 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
         table = self.get_table(request, permissions)
         table = self.get_table(request, permissions)
         paginate_table(table, request)
         paginate_table(table, request)
 
 
+        # If this is an HTMX request, return only the rendered table HTML
+        if is_htmx(request):
+            return render(request, 'htmx/table.html', {
+                'table': table,
+            })
+
         context = {
         context = {
             'content_type': content_type,
             'content_type': content_type,
             'table': table,
             'table': table,

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox-light.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox-print.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 1 - 0
netbox/project-static/package.json

@@ -30,6 +30,7 @@
     "cookie": "^0.4.1",
     "cookie": "^0.4.1",
     "dayjs": "^1.10.4",
     "dayjs": "^1.10.4",
     "flatpickr": "4.6.3",
     "flatpickr": "4.6.3",
+    "htmx.org": "^1.6.1",
     "just-debounce-it": "^1.4.0",
     "just-debounce-it": "^1.4.0",
     "masonry-layout": "^4.2.2",
     "masonry-layout": "^4.2.2",
     "query-string": "^6.14.1",
     "query-string": "^6.14.1",

+ 0 - 2
netbox/project-static/src/buttons/index.ts

@@ -1,7 +1,6 @@
 import { initConnectionToggle } from './connectionToggle';
 import { initConnectionToggle } from './connectionToggle';
 import { initDepthToggle } from './depthToggle';
 import { initDepthToggle } from './depthToggle';
 import { initMoveButtons } from './moveOptions';
 import { initMoveButtons } from './moveOptions';
-import { initPerPage } from './pagination';
 import { initPreferenceUpdate } from './preferences';
 import { initPreferenceUpdate } from './preferences';
 import { initReslug } from './reslug';
 import { initReslug } from './reslug';
 import { initSelectAll } from './selectAll';
 import { initSelectAll } from './selectAll';
@@ -13,7 +12,6 @@ export function initButtons(): void {
     initReslug,
     initReslug,
     initSelectAll,
     initSelectAll,
     initPreferenceUpdate,
     initPreferenceUpdate,
-    initPerPage,
     initMoveButtons,
     initMoveButtons,
   ]) {
   ]) {
     func();
     func();

+ 0 - 14
netbox/project-static/src/buttons/pagination.ts

@@ -1,14 +0,0 @@
-import { getElements } from '../util';
-
-function handlePerPageSelect(event: Event): void {
-  const select = event.currentTarget as HTMLSelectElement;
-  if (select.form !== null) {
-    select.form.submit();
-  }
-}
-
-export function initPerPage(): void {
-  for (const element of getElements<HTMLSelectElement>('select.per-page')) {
-    element.addEventListener('change', handlePerPageSelect);
-  }
-}

+ 1 - 0
netbox/project-static/src/index.ts

@@ -1,4 +1,5 @@
 import '@popperjs/core';
 import '@popperjs/core';
 import 'bootstrap';
 import 'bootstrap';
+import 'htmx.org';
 import 'simplebar';
 import 'simplebar';
 import './netbox';
 import './netbox';

+ 2 - 104
netbox/project-static/src/search.ts

@@ -1,5 +1,4 @@
-import debounce from 'just-debounce-it';
-import { getElements, getRowValues, findFirstAdjacent, isTruthy } from './util';
+import { getElements, findFirstAdjacent, isTruthy } from './util';
 
 
 /**
 /**
  * Change the display value and hidden input values of the search filter based on dropdown
  * Change the display value and hidden input values of the search filter based on dropdown
@@ -41,109 +40,8 @@ function initSearchBar(): void {
   }
   }
 }
 }
 
 
-/**
- * Initialize Interface Table Filter Elements.
- */
-function initInterfaceFilter(): void {
-  for (const input of getElements<HTMLInputElement>('input.interface-filter')) {
-    const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
-    const rows = Array.from(
-      table?.querySelectorAll<HTMLTableRowElement>('tbody > tr') ?? [],
-    ).filter(r => r !== null);
-    /**
-     * Filter on-page table by input text.
-     */
-    function handleInput(event: Event): void {
-      const target = event.target as HTMLInputElement;
-      // Create a regex pattern from the input search text to match against.
-      const filter = new RegExp(target.value.toLowerCase().trim());
-
-      // Each row represents an interface and its attributes.
-      for (const row of rows) {
-        // Find the row's checkbox and deselect it, so that it is not accidentally included in form
-        // submissions.
-        const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
-        if (checkBox !== null) {
-          checkBox.checked = false;
-        }
-
-        // The data-name attribute's value contains the interface name.
-        const name = row.getAttribute('data-name');
-
-        if (typeof name === 'string') {
-          if (filter.test(name.toLowerCase().trim())) {
-            // If this row matches the search pattern, but is already hidden, unhide it.
-            if (row.classList.contains('d-none')) {
-              row.classList.remove('d-none');
-            }
-          } else {
-            // If this row doesn't match the search pattern, hide it.
-            row.classList.add('d-none');
-          }
-        }
-      }
-    }
-    input.addEventListener('keyup', debounce(handleInput, 300));
-  }
-}
-
-function initTableFilter(): void {
-  for (const input of getElements<HTMLInputElement>('input.object-filter')) {
-    // Find the first adjacent table element.
-    const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
-
-    // Build a valid array of <tr/> elements that are children of the adjacent table.
-    const rows = Array.from(
-      table?.querySelectorAll<HTMLTableRowElement>('tbody > tr') ?? [],
-    ).filter(r => r !== null);
-
-    /**
-     * Filter table rows by matched input text.
-     * @param event
-     */
-    function handleInput(event: Event): void {
-      const target = event.target as HTMLInputElement;
-
-      // Create a regex pattern from the input search text to match against.
-      const filter = new RegExp(target.value.toLowerCase().trim());
-
-      // List of which rows which match the query
-      const matchedRows: Array<HTMLTableRowElement> = [];
-
-      for (const row of rows) {
-        // Find the row's checkbox and deselect it, so that it is not accidentally included in form
-        // submissions.
-        const checkBox = row.querySelector<HTMLInputElement>('input[type="checkbox"][name="pk"]');
-        if (checkBox !== null) {
-          checkBox.checked = false;
-        }
-
-        // Iterate through each row's cell values
-        for (const value of getRowValues(row)) {
-          if (filter.test(value.toLowerCase())) {
-            // If this row matches the search pattern, add it to the list.
-            matchedRows.push(row);
-            break;
-          }
-        }
-      }
-
-      // Iterate the rows again to set visibility.
-      // This results in a single reflow instead of one for each row.
-      for (const row of rows) {
-        if (matchedRows.indexOf(row) >= 0) {
-          row.classList.remove('d-none');
-        } else {
-          row.classList.add('d-none');
-        }
-      }
-    }
-    input.addEventListener('keyup', debounce(handleInput, 300));
-  }
-}
-
 export function initSearch(): void {
 export function initSearch(): void {
-  for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) {
+  for (const func of [initSearchBar]) {
     func();
     func();
   }
   }
 }
 }

+ 0 - 12
netbox/project-static/styles/netbox.scss

@@ -737,10 +737,6 @@ nav.breadcrumb-container {
   }
   }
 }
 }
 
 
-div.paginator > form > div.input-group {
-  width: fit-content;
-}
-
 label.required {
 label.required {
   font-weight: $font-weight-bold;
   font-weight: $font-weight-bold;
 
 
@@ -900,14 +896,6 @@ div.card-overlay {
   }
   }
 }
 }
 
 
-// Right-align the paginator element.
-.paginator {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  padding: $spacer 0;
-}
-
 // Tabbed content
 // Tabbed content
 .nav-tabs {
 .nav-tabs {
   .nav-link {
   .nav-link {

+ 5 - 0
netbox/project-static/yarn.lock

@@ -1688,6 +1688,11 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.8.9:
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
   integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
   integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 
 
+htmx.org@^1.6.1:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.6.1.tgz#6f0d59a93fa61cbaa15316c134a2f179045a5778"
+  integrity sha512-i+1k5ee2eFWaZbomjckyrDjUpa3FMDZWufatUSBmmsjXVksn89nsXvr1KLGIdAajiz+ZSL7TE4U/QaZVd2U2sA==
+
 ignore@^4.0.6:
 ignore@^4.0.6:
   version "4.0.6"
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"

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

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.circuits.add_circuit %}
+    <a href="{% url 'circuits:circuit_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Circuit
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -39,22 +48,13 @@
 <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">
-        Circuits
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=circuits_table %}
-      </div>
-      {% if perms.circuits.add_circuit %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'circuits:circuit_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Circuit
-          </a>
-        </div>
-      {% endif %}
+      <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>
-      {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
-      {% plugin_full_width_page object %}
+    </div>
+    {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 20 - 22
netbox/templates/circuits/provider.html

@@ -2,9 +2,18 @@
 {% load static %}
 {% load static %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.circuits.add_circuit %}
+    <a href="{% url 'circuits:circuit_add' %}?provider={{ object.pk }}" class="btn btn-sm btn-primary">
+      <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add circuit
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
-<div class="row">
+<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">
             <h5 class="card-header">
@@ -56,28 +65,17 @@
         {% include 'inc/panels/contacts.html' %}
         {% include 'inc/panels/contacts.html' %}
         {% plugin_right_page object %}
         {% plugin_right_page object %}
     </div>
     </div>
-    <div class="col col-md-12">
-        <div class="card">
-            <h5 class="card-header">
-                Circuits
-            </h5>
-            <div class="card-body">
-            {% include 'inc/table.html' with table=circuits_table %}
-            </div>
-            {% if perms.circuits.add_circuit %}
-            <div class="card-footer text-end noprint">
-                <a href="{% url 'circuits:circuit_add' %}?provider={{ object.pk }}" class="btn btn-sm btn-primary">
-                    <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add circuit
-                </a>
-            </div>
-            {% endif %}
-        </div>
-        {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
-    </div>
 </div>
 </div>
-<div class="row">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
+<div class="row mb-3">
+  <div class="col col-md-12">
+    <div class="card">
+      <h5 class="card-header">Circuits</h5>
+      <div class="card-body table-responsive">
+        {% render_table circuits_table 'inc/table.html' %}
+        {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
+      </div>
     </div>
     </div>
+    {% plugin_full_width_page object %}
+  </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 11 - 16
netbox/templates/circuits/providernetwork.html

@@ -2,6 +2,7 @@
 {% load static %}
 {% load static %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,7 +10,7 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
-<div class="row">
+<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">
             <h5 class="card-header">
@@ -43,22 +44,16 @@
         {% plugin_right_page object %}
         {% plugin_right_page object %}
     </div>
     </div>
 </div>
 </div>
-<div class="row">
-    <div class="col col-md-12">
-        <div class="card">
-            <h5 class="card-header">
-                Circuits
-            </h5>
-            <div class="card-body">
-                {% include 'inc/table.html' with table=circuits_table %}
-            </div>
-        </div>
+<div class="row mb-3">
+  <div class="col col-md-12">
+    <div class="card">
+      <h5 class="card-header">Circuits</h5>
+      <div class="card-body table-responsive">
+        {% render_table circuits_table 'inc/table.html' %}
         {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
         {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
+      </div>
     </div>
     </div>
-</div>
-<div class="row">
-    <div class="col col-md-12">
-        {% plugin_full_width_page object %}
-    </div>
+    {% plugin_full_width_page object %}
+  </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 4 - 9
netbox/templates/dcim/connections_list.html

@@ -8,19 +8,14 @@
 {% block content-wrapper %}
 {% block content-wrapper %}
   <div class="tab-content">
   <div class="tab-content">
 
 
-    {# Conncetions list #}
+    {# Connections list #}
     <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.html' %}
-
+      {% include 'inc/table_controls_htmx.html' %}
       <div class="card">
       <div class="card">
-        <div class="card-body">
-          <div class="table-responsive">
-            {% render_table table 'inc/table.html' %}
-          </div>
+        <div class="card-body" id="object_list">
+          {% include 'htmx/table.html' %}
         </div>
         </div>
       </div>
       </div>
-
-      {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
     </div>
     </div>
 
 
     {# Filter form #}
     {# Filter form #}

+ 18 - 18
netbox/templates/dcim/device.html

@@ -179,31 +179,31 @@
                         <tr>
                         <tr>
                             <th scope="row">Primary IPv4</th>
                             <th scope="row">Primary IPv4</th>
                             <td>
                             <td>
-                                {% if object.primary_ip4 %}
-                                    <a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
-                                    {% if object.primary_ip4.nat_inside %}
-                                        <span>(NAT for {{ object.primary_ip4.nat_inside.address.ip }})</span>
-                                    {% elif object.primary_ip4.nat_outside %}
-                                        <span>(NAT: {{ object.primary_ip4.nat_outside.address.ip }})</span>
-                                    {% endif %}
-                                {% else %}
-                                    <span class="text-muted">&mdash;</span>
+                              {% if object.primary_ip4 %}
+                                <a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
+                                {% if object.primary_ip4.nat_inside %}
+                                  (NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }})">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
+                                {% elif object.primary_ip4.nat_outside %}
+                                  (NAT: <a href="{{ object.primary_ip4.nat_outside.get_absolute_url }}">{{ object.primary_ip4.nat_outside.address.ip }}</a>)
                                 {% endif %}
                                 {% endif %}
+                              {% else %}
+                                <span class="text-muted">&mdash;</span>
+                              {% endif %}
                             </td>
                             </td>
                         </tr>
                         </tr>
                         <tr>
                         <tr>
                             <th scope="row">Primary IPv6</th>
                             <th scope="row">Primary IPv6</th>
                             <td>
                             <td>
-                                {% if object.primary_ip6 %}
-                                    <a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
-                                    {% if object.primary_ip6.nat_inside %}
-                                        <span>(NAT for {{ object.primary_ip6.nat_inside.address.ip }})</span>
-                                    {% elif object.primary_ip6.nat_outside %}
-                                        <span>(NAT: {{ object.primary_ip6.nat_outside.address.ip }})</span>
-                                    {% endif %}
-                                {% else %}
-                                    <span class="text-muted">&mdash;</span>
+                              {% if object.primary_ip6 %}
+                                <a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
+                                {% if object.primary_ip6.nat_inside %}
+                                  (NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
+                                {% elif object.primary_ip6.nat_outside %}
+                                  (NAT: <a href="{{ object.primary_ip6.nat_outside.get_absolute_url }}">{{ object.primary_ip6.nat_outside.address.ip }}</a>)
                                 {% endif %}
                                 {% endif %}
+                              {% else %}
+                                <span class="text-muted">&mdash;</span>
+                              {% endif %}
                             </td>
                             </td>
                         </tr>
                         </tr>
                         {% if object.cluster %}
                         {% if object.cluster %}

+ 8 - 3
netbox/templates/dcim/device/consoleports.html

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

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

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

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

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

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

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

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

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

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

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

+ 3 - 3
netbox/templates/dcim/device/lldp_neighbors.html

@@ -31,12 +31,12 @@
                 <tbody>
                 <tbody>
                     {% for iface in interfaces %}
                     {% for iface in interfaces %}
                         <tr id="{{ iface.name }}">
                         <tr id="{{ iface.name }}">
-                            <td class="font-monospace">{{ iface }}</td>
+                            <td>{{ iface }}</td>
                             {% if iface.connected_endpoint.device %}
                             {% if iface.connected_endpoint.device %}
-                                <td class="configured_device" data="{{ iface.connected_endpoint.device }}" data-chassis="{{ iface.connected_endpoint.device.virtual_chassis.name }}">
+                                <td class="configured_device" data="{{ iface.connected_endpoint.device.name }}" data-chassis="{{ iface.connected_endpoint.device.virtual_chassis.name }}">
                                     <a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
                                     <a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
                                 </td>
                                 </td>
-                                <td class="configured_interface" data="{{ iface.connected_endpoint }}">
+                                <td class="configured_interface" data="{{ iface.connected_endpoint.name }}">
                                     <span title="{{ iface.connected_endpoint.get_type_display }}">{{ iface.connected_endpoint }}</span>
                                     <span title="{{ iface.connected_endpoint.get_type_display }}">{{ iface.connected_endpoint }}</span>
                                 </td>
                                 </td>
                             {% elif iface.connected_endpoint.circuit %}
                             {% elif iface.connected_endpoint.circuit %}

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

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

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

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

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

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

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

@@ -1,11 +1,20 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   <li class="breadcrumb-item"><a href="{% url 'dcim:devicerole_list' %}">Device Roles</a></li>
   <li class="breadcrumb-item"><a href="{% url 'dcim:devicerole_list' %}">Device Roles</a></li>
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.dcim.add_device %}
+    <a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -69,21 +78,12 @@
 <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">
-        Devices
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=devices_table %}
+      <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>
-      {% if perms.dcim.add_device %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

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

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

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

@@ -450,7 +450,7 @@
                 <h5 class="card-header">
                 <h5 class="card-header">
                     IP Addresses
                     IP Addresses
                 </h5>
                 </h5>
-                <div class="card-body">
+                <div class="card-body table-responsive">
                     {% if ipaddress_table.rows %}
                     {% if ipaddress_table.rows %}
                         {% render_table ipaddress_table 'inc/table.html' %}
                         {% render_table ipaddress_table 'inc/table.html' %}
                     {% else %}
                     {% else %}

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

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,6 +10,14 @@
   {% endfor %}
   {% endfor %}
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.dcim.add_location %}
+    <a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Child Location
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -88,22 +97,13 @@
 <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">
-        {% include 'inc/table.html' with table=child_locations_table %}
-      </div>
-      {% if perms.dcim.add_location %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:location_add' %}?site={{ object.site.pk }}&parent={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Location
-          </a>
-        </div>
-      {% endif %}
+      <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>
       </div>
-      {% include 'inc/paginator.html' with paginator=child_locations_table.paginator page=child_locations_table.page %}
-      {% plugin_full_width_page object %}
+    </div>
+    {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

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

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.dcim.add_devicetype %}
+    <a href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device Type
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -46,21 +55,12 @@
 <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">
-        Device Types
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=devicetypes_table %}
+      <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>
-      {% if perms.dcim.add_devicetype %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add device type
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=devicetypes_table.paginator page=devicetypes_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

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

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,6 +10,14 @@
   {% endif %}
   {% endif %}
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.dcim.add_device %}
+    <a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -74,22 +83,13 @@
 <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">
-        Devices
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=devices_table %}
+      <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>
-      {% if perms.dcim.add_device %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:device_add' %}?device_role={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device
-          </a>
-        </div>
-      {% endif %}
-    </div>
-    {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
-        {% plugin_full_width_page object %}
     </div>
     </div>
+    {% plugin_full_width_page object %}
+  </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

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

@@ -54,7 +54,7 @@
         <form method="post">
         <form method="post">
             {% csrf_token %}
             {% csrf_token %}
             <div class="card">
             <div class="card">
-                <div class="card-body">
+                <div class="card-body table-responsive">
                     {% render_table powerfeed_table 'inc/table.html' %}
                     {% render_table powerfeed_table 'inc/table.html' %}
                 </div>
                 </div>
                 <div class="card-footer noprint">
                 <div class="card-footer noprint">

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

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.dcim.add_rack %}
+    <a href="{% url 'dcim:rack_add' %}?role={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Rack
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -45,21 +54,12 @@
 <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">
-        Racks
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=racks_table %}
+      <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>
-      {% if perms.dcim.add_rack %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:rack_add' %}?role={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Rack
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=racks_table.paginator page=racks_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

+ 18 - 18
netbox/templates/dcim/region.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,6 +10,14 @@
   {% endfor %}
   {% endfor %}
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.dcim.add_site %}
+    <a href="{% url 'dcim:site_add' %}?region={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Site
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -55,8 +64,8 @@
       <h5 class="card-header">
       <h5 class="card-header">
         Child Regions
         Child Regions
       </h5>
       </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=child_regions_table %}
+      <div class="card-body table-responsive">
+        {% render_table child_regions_table 'inc/table.html' %}
       </div>
       </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">
@@ -69,25 +78,16 @@
     {% plugin_right_page object %}
     {% plugin_right_page object %}
 	</div>
 	</div>
 </div>
 </div>
-<div class="row">
+<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">
-        Sites
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=sites_table %}
+      <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>
-      {% if perms.dcim.add_site %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:site_add' %}?region={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Site
-          </a>
-        </div>
-      {% endif %}
-      </div>
-      {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
-      {% plugin_full_width_page object %}
+    </div>
+    {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 15 - 15
netbox/templates/dcim/sitegroup.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,6 +10,14 @@
   {% endfor %}
   {% endfor %}
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.dcim.add_site %}
+    <a href="{% url 'dcim:site_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Site
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -55,8 +64,8 @@
       <h5 class="card-header">
       <h5 class="card-header">
         Child Groups
         Child Groups
       </h5>
       </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=child_groups_table %}
+      <div class="card-body table-responsive">
+        {% render_table child_groups_table 'inc/table.html' %}
       </div>
       </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">
@@ -72,21 +81,12 @@
 <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">
-        Sites
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=sites_table %}
+      <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>
-      {% if perms.dcim.add_site %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'dcim:site_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Site
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

+ 11 - 3
netbox/templates/extras/object_changelog.html

@@ -2,9 +2,17 @@
 {% load render_table from django_tables2 %}
 {% load render_table from django_tables2 %}
 
 
 {% block content %}
 {% block content %}
-    {% render_table table 'inc/table.html' %}
-    {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
-    <div class="text-muted">
+  <div class="row mb-3">
+    <div class="col col-md-12">
+      <div class="card">
+        <div class="card-body table-responsive">
+          {% render_table table 'inc/table.html' %}
+          {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+        </div>
+      </div>
+      <div class="text-muted">
         Change log retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
         Change log retention: {% if settings.CHANGELOG_RETENTION %}{{ settings.CHANGELOG_RETENTION }} days{% else %}Indefinite{% endif %}
+      </div>
     </div>
     </div>
+  </div>
 {% endblock %}
 {% endblock %}

+ 6 - 2
netbox/templates/extras/object_journal.html

@@ -24,6 +24,10 @@
       </div>
       </div>
     </form>
     </form>
   {% endif %}
   {% endif %}
-  {% render_table table 'inc/table.html' %}
-  {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+  <div class="card">
+    <div class="card-body table-responsive">
+      {% render_table table 'inc/table.html' %}
+      {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
+    </div>
+  </div>
 {% endblock %}
 {% endblock %}

+ 11 - 4
netbox/templates/extras/tag.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block content %}
 {% block content %}
   <div class="row">
   <div class="row">
@@ -63,12 +64,18 @@
           </table>
           </table>
         </div>
         </div>
       </div>
       </div>
+    </div>
   </div>
   </div>
   <div class="row">
   <div class="row">
-    <div class="col">
-      {% include 'inc/panel_table.html' with table=taggeditem_table heading='Tagged Objects' %}
-      {% include 'inc/paginator.html' with paginator=taggeditem_table.paginator page=items_table.page %}
+    <div class="col col-md-12">
+      <div class="card">
+        <h5 class="card-header">Tagged Objects</h5>
+        <div class="card-body table-responsive">
+          {% render_table taggeditem_table 'inc/table.html' %}
+          {% include 'inc/paginator.html' with paginator=taggeditem_table.paginator page=taggeditem_table.page %}
+        </div>
+      </div>
+      {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>
-  {% plugin_full_width_page object %}
 {% endblock %}
 {% endblock %}

+ 3 - 2
netbox/templates/generic/object_bulk_add_component.html

@@ -1,5 +1,6 @@
 {% extends 'base/layout.html' %}
 {% extends 'base/layout.html' %}
 {% load form_helpers %}
 {% load form_helpers %}
+{% load render_table from django_tables2 %}
 
 
 {% block title %}Add {{ model_name|title }}{% endblock %}
 {% block title %}Add {{ model_name|title }}{% endblock %}
 
 
@@ -15,8 +16,8 @@
     {% endfor %}
     {% endfor %}
     <div class="row">
     <div class="row">
         <div class="col col-md-7">
         <div class="col col-md-7">
-            <div class="card">
-                {% include 'inc/table.html' %}
+            <div class="table-responsive">
+              {% render_table table 'inc/table.html' %}
             </div>
             </div>
         </div>
         </div>
         <div class="col col-md-5">
         <div class="col col-md-5">

+ 4 - 1
netbox/templates/generic/object_bulk_delete.html

@@ -1,5 +1,6 @@
 {% extends 'base/layout.html' %}
 {% extends 'base/layout.html' %}
 {% load helpers %}
 {% load helpers %}
+{% load render_table from django_tables2 %}
 
 
 {% block title %}Delete {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
 {% block title %}Delete {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
 
 
@@ -15,7 +16,9 @@
         </div>
         </div>
     </div>
     </div>
     <div class="container-xl px-0">
     <div class="container-xl px-0">
-      {% include 'inc/table.html' %}
+      <div class="table-responsive">
+        {% render_table table 'inc/table.html' %}
+      </div>
       <div class="row mt-3">
       <div class="row mt-3">
         <form action="" method="post">
         <form action="" method="post">
             {% csrf_token %}
             {% csrf_token %}

+ 4 - 1
netbox/templates/generic/object_bulk_edit.html

@@ -1,6 +1,7 @@
 {% extends 'base/layout.html' %}
 {% extends 'base/layout.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load form_helpers %}
 {% load form_helpers %}
+{% load render_table from django_tables2 %}
 
 
 {% block title %}Editing {{ table.rows|length }} {{ obj_type_plural|bettertitle }}{% endblock %}
 {% block title %}Editing {{ table.rows|length }} {{ obj_type_plural|bettertitle }}{% endblock %}
 
 
@@ -59,7 +60,9 @@
 
 
       {# Selected objects list #}
       {# Selected objects list #}
       <div class="tab-pane" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
       <div class="tab-pane" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
-        {% include 'inc/table.html' %}
+        <div class="table-responsive">
+          {% render_table table 'inc/table.html' %}
+        </div>
       </div>
       </div>
 
 
     {% endblock %}
     {% endblock %}

+ 4 - 1
netbox/templates/generic/object_bulk_remove.html

@@ -1,5 +1,6 @@
 {% extends 'base/layout.html' %}
 {% extends 'base/layout.html' %}
 {% load helpers %}
 {% load helpers %}
+{% load render_table from django_tables2 %}
 
 
 {% block title %}Remove {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
 {% block title %}Remove {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
 
 
@@ -13,7 +14,9 @@
   </div>
   </div>
 </div>
 </div>
 <div class="container-xl px-0">
 <div class="container-xl px-0">
-  {% include 'inc/table.html' %}
+  <div class="table-responsive">
+    {% render_table table 'inc/table.html' %}
+  </div>
   <form action="." method="post" class="form">
   <form action="." method="post" class="form">
     {% csrf_token %}
     {% csrf_token %}
     {% for field in form.hidden_fields %}
     {% for field in form.hidden_fields %}

+ 3 - 7
netbox/templates/generic/object_list.html

@@ -87,7 +87,7 @@
       {% endif %}
       {% endif %}
 
 
       {# Object table controls #}
       {# Object table controls #}
-      {% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
+      {% include 'inc/table_controls_htmx.html' with table_modal="ObjectTable_config" %}
 
 
       <form method="post" class="form form-horizontal">
       <form method="post" class="form form-horizontal">
         {% csrf_token %}
         {% csrf_token %}
@@ -95,10 +95,8 @@
 
 
         {# Object table #}
         {# Object table #}
         <div class="card">
         <div class="card">
-          <div class="card-body">
-            <div class="table-responsive">
-              {% render_table table 'inc/table.html' %}
-            </div>
+          <div class="card-body" id="object_list">
+            {% include 'htmx/table.html' %}
           </div>
           </div>
         </div>
         </div>
 
 
@@ -125,8 +123,6 @@
 
 
       </form>
       </form>
 
 
-      {# Paginator #}
-      {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
     </div>
     </div>
 
 
     {# Filter form #}
     {# Filter form #}

+ 5 - 0
netbox/templates/htmx/table.html

@@ -0,0 +1,5 @@
+{# Render an HTMX-enabled table with paginator #}
+{% load render_table from django_tables2 %}
+
+{% render_table table 'inc/table_htmx.html' %}
+{% include 'inc/paginator_htmx.html' with paginator=table.paginator page=table.page %}

+ 41 - 40
netbox/templates/inc/paginator.html

@@ -1,51 +1,52 @@
 {% load helpers %}
 {% load helpers %}
 
 
-<div class="paginator float-end text-end">
+<div class="row">
+  <div class="col col-md-6 mb-0">
+    {# Page number carousel #}
     {% if paginator.num_pages > 1 %}
     {% if paginator.num_pages > 1 %}
-    <div class="btn-group btn-group-sm mb-3" role="group" aria-label="Pages">    
-    {% if page.has_previous %}
-        <a href="{% querystring request page=page.previous_page_number %}" class="btn btn-outline-secondary">
+      <div class="btn-group btn-group-sm mb-3" role="group" aria-label="Pages">
+        {% if page.has_previous %}
+          <a href="{% querystring request page=page.previous_page_number %}" class="btn btn-outline-secondary">
             <i class="mdi mdi-chevron-double-left"></i>
             <i class="mdi mdi-chevron-double-left"></i>
-        </a>
-    {% endif %}
-    {% for p in page.smart_pages %}
-        {% if p %}
-        <a href="{% querystring request page=p %}" class="btn btn-outline-secondary{% if page.number == p %} active{% endif %}">
-            {{ p }}
-        </a>
-        {% else %}
-        <button type="button" class="btn btn-outline-secondary" disabled>
-            <span>&hellip;</span>
-        </button>
+          </a>
         {% endif %}
         {% endif %}
-    {% endfor %}
-    {% if page.has_next %}
-        <a href="{% querystring request page=page.next_page_number %}" class="btn btn-outline-secondary">
+        {% for p in page.smart_pages %}
+          {% if p %}
+            <a href="{% querystring request page=p %}" class="btn btn-outline-secondary{% if page.number == p %} active{% endif %}">
+              {{ p }}
+            </a>
+          {% else %}
+            <button type="button" class="btn btn-outline-secondary" disabled>
+              <span>&hellip;</span>
+            </button>
+          {% endif %}
+        {% endfor %}
+        {% if page.has_next %}
+          <a href="{% querystring request page=page.next_page_number %}" class="btn btn-outline-secondary">
             <i class="mdi mdi-chevron-double-right"></i>
             <i class="mdi mdi-chevron-double-right"></i>
-        </a>
-    {% endif %}
-    </div>
+          </a>
+        {% endif %}
+      </div>
     {% endif %}
     {% endif %}
-    <form method="get" class="mb-2">
-        {% for k, v_list in request.GET.lists %}
-            {% if k != 'per_page' %}
-                {% for v in v_list %}
-                    <input type="hidden" name="{{ k }}" value="{{ v }}" />
-                {% endfor %}
-            {% endif %}
-        {% endfor %}
-        <div class="input-group input-group-sm">
-            <select name="per_page" class="form-select per-page">
-            {% for n in page.paginator.get_page_lengths %}
-                <option value="{{ n }}"{% if page.paginator.per_page == n %} selected="selected"{% endif %}>{{ n }}</option>
-            {% endfor %}
-            </select>
-            <label class="input-group-text" for="per_page">Per Page</label>
-        </div>
-    </form>
+  </div>
+  <div class="col col-md-6 mb-0 text-end">
+    {# Per-page count selector #}
     {% if page %}
     {% if page %}
-    <small class="text-end text-muted">
+      <div class="dropdown dropup">
+        <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
+          Per Page
+        </button>
+        <ul class="dropdown-menu">
+          {% for n in page.paginator.get_page_lengths %}
+            <li>
+              <a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
+            </li>
+          {% endfor %}
+        </ul>
+      </div>
+      <small class="text-end text-muted">
         Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
         Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
-    </small>
+      </small>
     {% endif %}
     {% endif %}
+  </div>
 </div>
 </div>

+ 72 - 0
netbox/templates/inc/paginator_htmx.html

@@ -0,0 +1,72 @@
+{% load helpers %}
+
+<div class="row">
+  <div class="col col-md-6 mb-0">
+    {# Page number carousel #}
+    {% if paginator.num_pages > 1 %}
+      <div class="btn-group btn-group-sm" role="group" aria-label="Pages">
+        {% if page.has_previous %}
+          <a href="#"
+             hx-get="{% querystring request page=page.previous_page_number %}"
+             hx-target="#object_list"
+             hx-push-url="true"
+             class="btn btn-outline-secondary"
+          >
+            <i class="mdi mdi-chevron-double-left"></i>
+          </a>
+        {% endif %}
+        {% for p in page.smart_pages %}
+          {% if p %}
+            <a href="#"
+               hx-get="{% querystring request page=p %}"
+               hx-target="#object_list"
+               hx-push-url="true"
+               class="btn btn-outline-secondary{% if page.number == p %} active{% endif %}"
+            >
+              {{ p }}
+            </a>
+          {% else %}
+            <button type="button" class="btn btn-outline-secondary" disabled>
+              <span>&hellip;</span>
+            </button>
+          {% endif %}
+        {% endfor %}
+        {% if page.has_next %}
+          <a href="#"
+             hx-get="{% querystring request page=page.next_page_number %}"
+             hx-target="#object_list"
+             hx-push-url="true"
+             class="btn btn-outline-secondary"
+          >
+            <i class="mdi mdi-chevron-double-right"></i>
+          </a>
+        {% endif %}
+      </div>
+    {% endif %}
+  </div>
+  <div class="col col-md-6 mb-0 text-end">
+    {# Per-page count selector #}
+    {% if page %}
+      <div class="dropdown dropup">
+        <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
+          Per Page
+        </button>
+        <ul class="dropdown-menu">
+          {% for n in page.paginator.get_page_lengths %}
+            <li>
+              <a href="#"
+                 hx-get="{% querystring request per_page=n %}"
+                 hx-target="#object_list"
+                 hx-push-url="true"
+                 class="dropdown-item"
+              >{{ n }}</a>
+            </li>
+          {% endfor %}
+        </ul>
+      </div>
+      <small class="text-end text-muted">
+        Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
+      </small>
+    {% endif %}
+  </div>
+</div>

+ 4 - 4
netbox/templates/inc/panel_table.html

@@ -6,11 +6,11 @@
         {{ heading }}
         {{ heading }}
     </h5>
     </h5>
     {% endif %}
     {% endif %}
-    <div class="card-body">
-    {% if table.rows %}
+    <div class="card-body table-responsive">
+      {% if table.rows %}
         {% render_table table 'inc/table.html' %}
         {% render_table table 'inc/table.html' %}
-    {% else %}
+      {% else %}
         <div class="text-muted">None</div>
         <div class="text-muted">None</div>
-    {% endif %}
+      {% endif %}
     </div>
     </div>
 </div>
 </div>

+ 37 - 39
netbox/templates/inc/table.html

@@ -1,43 +1,41 @@
 {% 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 }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ 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 }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ 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>

+ 8 - 3
netbox/templates/inc/table_controls.html → netbox/templates/inc/table_controls_htmx.html

@@ -1,11 +1,16 @@
+{% load helpers %}
+
 <div class="row mb-3 justify-content-between">
 <div class="row mb-3 justify-content-between">
   <div class="table-controls noprint col col-12 col-md-8 col-lg-4">
   <div class="table-controls noprint col col-12 col-md-8 col-lg-4">
     <div class="input-group input-group-sm">
     <div class="input-group input-group-sm">
       <input
       <input
         type="text"
         type="text"
-        class="form-control object-filter"
-        placeholder="Quick find"
-        title="Find in the results below (regular expressions supported)"
+        name="q"
+        class="form-control"
+        placeholder="Quick search"
+        hx-get="{{ request.full_path }}"
+        hx-target="#object_list"
+        hx-trigger="keyup changed delay:500ms"
       />
       />
     </div>
     </div>
   </div>
   </div>

+ 49 - 0
netbox/templates/inc/table_htmx.html

@@ -0,0 +1,49 @@
+{% load django_tables2 %}
+
+<div class="table-responsive">
+  <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 }}>
+                <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 %}
+        </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>

+ 53 - 69
netbox/templates/ipam/aggregate.html

@@ -1,82 +1,66 @@
-{% extends 'generic/object.html' %}
+{% extends 'ipam/aggregate/base.html' %}
 {% load buttons %}
 {% load buttons %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
 
 
-{% block breadcrumbs %}
-  {{ block.super }}
-  <li class="breadcrumb-item"><a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.rir.pk }}">{{ object.rir }}</a></li>
-{% endblock %}
-
-{% block extra_controls %}
-  {% include 'ipam/inc/toggle_available.html' %}
-{% endblock %}
-
 {% block content %}
 {% block content %}
-<div class="row">
-	<div class="col col-md-6">
-        <div class="card">
-            <h5 class="card-header">
-                Aggregate
-            </h5>
-            <div class="card-body">
-                <table class="table table-hover attr-table">
-                    <tr>
-                        <td>Family</td>
-                        <td>IPv{{ object.family }}</td>
-                    </tr>
-                    <tr>
-                        <td>RIR</td>
-                        <td>
-                            <a href="{% url 'ipam:aggregate_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
-                        </td>
-                    </tr>
-                    <tr>
-                        <td>Utilization</td>
-                        <td>
-                            {% utilization_graph object.get_utilization %}
-                        </td>
-                    </tr>
-                    <tr>
-                        <td>Tenant</td>
-                        <td>
-                            {% if object.tenant %}
-                                {% if prefix.object.group %}
-                                    <a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
-                                {% endif %}
-                                <a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
-                            {% else %}
-                                <span class="text-muted">None</span>
-                            {% endif %}
-                        </td>
-                    </tr>
-                    <tr>
-                        <td>Date Added</td>
-                        <td>{{ object.date_added|annotated_date|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <td>Description</td>
-                        <td>{{ object.description|placeholder }}</td>
-                    </tr>
-                </table>
-            </div>
+  <div class="row">
+    <div class="col col-md-6">
+      <div class="card">
+        <h5 class="card-header">Aggregate</h5>
+        <div class="card-body">
+          <table class="table table-hover attr-table">
+            <tr>
+              <td>Family</td>
+              <td>IPv{{ object.family }}</td>
+            </tr>
+            <tr>
+              <td>RIR</td>
+              <td>
+                <a href="{% url 'ipam:aggregate_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
+              </td>
+            </tr>
+            <tr>
+              <td>Utilization</td>
+              <td>
+                {% utilization_graph object.get_utilization %}
+              </td>
+            </tr>
+            <tr>
+              <td>Tenant</td>
+              <td>
+                {% if object.tenant %}
+                  {% if prefix.object.group %}
+                    <a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
+                  {% endif %}
+                  <a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
+                {% else %}
+                  <span class="text-muted">None</span>
+                {% endif %}
+              </td>
+            </tr>
+            <tr>
+              <td>Date Added</td>
+              <td>{{ object.date_added|annotated_date|placeholder }}</td>
+            </tr>
+            <tr>
+              <td>Description</td>
+              <td>{{ object.description|placeholder }}</td>
+            </tr>
+          </table>
         </div>
         </div>
-        {% plugin_left_page object %}
+      </div>
+      {% plugin_left_page object %}
     </div>
     </div>
     <div class="col col-md-6">
     <div class="col col-md-6">
-        {% include 'inc/panels/custom_fields.html' %}
-        {% include 'inc/panels/tags.html' %}
-        {% plugin_right_page object %}
+      {% include 'inc/panels/custom_fields.html' %}
+      {% include 'inc/panels/tags.html' %}
+      {% plugin_right_page object %}
     </div>
     </div>
-</div>
-<div class="row mb-3">
+  </div>
+  <div class="row mb-3">
     <div class="col col-md-12">
     <div class="col col-md-12">
-        {% plugin_full_width_page object %}
+      {% plugin_full_width_page object %}
     </div>
     </div>
-</div>
-<div class="row mb-3">
-  <div class="col col-md-12">
-    {% include 'utilities/obj_table.html' with table=prefix_table heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}
   </div>
   </div>
-</div>
 {% endblock %}
 {% endblock %}

+ 23 - 0
netbox/templates/ipam/aggregate/base.html

@@ -0,0 +1,23 @@
+{% extends 'generic/object.html' %}
+{% load buttons %}
+{% load helpers %}
+
+{% block breadcrumbs %}
+  {{ block.super }}
+  <li class="breadcrumb-item"><a href="{% url 'ipam:aggregate_list' %}?rir_id={{ object.rir.pk }}">{{ object.rir }}</a></li>
+{% endblock %}
+
+{% block tab_items %}
+  <li role="presentation" class="nav-item">
+    <a class="nav-link{% if not active_tab %} active{% endif %}" href="{{ object.get_absolute_url }}">
+      Aggregate
+    </a>
+  </li>
+  {% if perms.ipam.view_prefix %}
+    <li role="presentation" class="nav-item">
+      <a class="nav-link{% if active_tab == 'prefixes' %} active{% endif %}" href="{% url 'ipam:aggregate_prefixes' pk=object.pk %}">
+        Prefixes {% badge object.get_child_prefixes.count %}
+      </a>
+    </li>
+  {% endif %}
+{% endblock %}

+ 36 - 0
netbox/templates/ipam/aggregate/prefixes.html

@@ -0,0 +1,36 @@
+{% extends 'ipam/aggregate/base.html' %}
+{% load helpers %}
+
+{% block extra_controls %}
+  {% include 'ipam/inc/toggle_available.html' %}
+  {{ block.super }}
+{% endblock %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_prefix %}
+          <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+          </button>
+        {% endif %}
+        {% if perms.ipam.delete_prefix %}
+          <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
+            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+          </button>
+        {% endif %}
+      </div>
+    </div>
+  </form>
+  {% table_config_form table %}
+{% endblock %}

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

@@ -2,6 +2,7 @@
 {% load buttons %}
 {% load buttons %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -67,11 +68,11 @@
     <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">
-          {% include 'inc/table.html' with table=sites_table %}
+        <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>
       </div>
       </div>
-      {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
       {% plugin_full_width_page object %}
       {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>

+ 2 - 2
netbox/templates/ipam/fhrpgroup.html

@@ -64,7 +64,7 @@
     <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">
+        <div class="card-body table-responsive">
           {% if ipaddress_table.rows %}
           {% if ipaddress_table.rows %}
             {% render_table ipaddress_table 'inc/table.html' %}
             {% render_table ipaddress_table 'inc/table.html' %}
           {% else %}
           {% else %}
@@ -81,7 +81,7 @@
       </div>
       </div>
       <div class="card">
       <div class="card">
         <h5 class="card-header">Members</h5>
         <h5 class="card-header">Members</h5>
-        <div class="card-body">
+        <div class="card-body table-responsive">
           {% if members_table.rows %}
           {% if members_table.rows %}
             {% render_table members_table 'inc/table.html' %}
             {% render_table members_table 'inc/table.html' %}
           {% else %}
           {% else %}

+ 8 - 5
netbox/templates/ipam/inc/toggle_available.html

@@ -1,12 +1,15 @@
 {% load helpers %}
 {% load helpers %}
 
 
-{% if show_available is not None %}
+{% if show_assigned or show_available is not None %}
   <div class="btn-group" role="group">
   <div class="btn-group" role="group">
-    <a href="{{ request.path }}{% querystring request show_available='true' %}" class="btn btn-sm btn-outline-primary{% if show_available %} active disabled{% endif %}">
-      <i class="mdi mdi-eye"></i> Show Available
+    <a href="{{ request.path }}{% querystring request show_assigned='true' show_available='false' %}" class="btn btn-sm {% if show_assigned and not show_available %}btn-primary active{% else %}btn-outline-primary{% endif %}">
+      Show Assigned
     </a>
     </a>
-    <a href="{{ request.path }}{% querystring request show_available='false' %}" class="btn btn-sm btn-outline-primary{% if not show_available %} active disabled{% endif %}">
-      <i class="mdi mdi-eye-off"></i> Hide Available
+    <a href="{{ request.path }}{% querystring request show_assigned='false' show_available='true' %}" class="btn btn-sm {% if show_available and not show_assigned %}btn-primary active{% else %}btn-outline-primary{% endif %}">
+      Show Available
+    </a>
+    <a href="{{ request.path }}{% querystring request show_assigned='true' show_available='true' %}" class="btn btn-sm {% if show_available and show_assigned %}btn-primary active{% else %}btn-outline-primary{% endif %}">
+      Show All
     </a>
     </a>
   </div>
   </div>
 {% endif %}
 {% endif %}

+ 4 - 4
netbox/templates/ipam/ipaddress.html

@@ -87,7 +87,7 @@
                         <th scope="row">NAT (inside)</th>
                         <th scope="row">NAT (inside)</th>
                         <td>
                         <td>
                             {% if object.nat_inside %}
                             {% if object.nat_inside %}
-                                <a href="{% url 'ipam:ipaddress' pk=object.nat_inside.pk %}">{{ object.nat_inside }}</a>
+                                <a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
                                 {% if object.nat_inside.assigned_object %}
                                 {% if object.nat_inside.assigned_object %}
                                     (<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
                                     (<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
                                 {% endif %}
                                 {% endif %}
@@ -100,7 +100,7 @@
                         <th scope="row">NAT (outside)</th>
                         <th scope="row">NAT (outside)</th>
                         <td>
                         <td>
                             {% if object.nat_outside %}
                             {% if object.nat_outside %}
-                                <a href="{% url 'ipam:ipaddress' pk=object.nat_outside.pk %}">{{ object.nat_outside }}</a>
+                                <a href="{{ object.nat_outside.get_absolute_url }}">{{ object.nat_outside }}</a>
                             {% else %}
                             {% else %}
                                 <span class="text-muted">None</span>
                                 <span class="text-muted">None</span>
                             {% endif %}
                             {% endif %}
@@ -133,8 +133,8 @@
                       </div>
                       </div>
                     {% endif %}
                     {% endif %}
                 </h5>
                 </h5>
-                <div class="card-body">
-                {% render_table duplicate_ips_table 'inc/table.html' %}
+                <div class="card-body table-responsive">
+                  {% render_table duplicate_ips_table 'inc/table.html' %}
                 </div>
                 </div>
             </div>
             </div>
         {% endif %}
         {% endif %}

+ 4 - 1
netbox/templates/ipam/ipaddress_assign.html

@@ -2,6 +2,7 @@
 {% load static %}
 {% load static %}
 {% load form_helpers %}
 {% load form_helpers %}
 {% load helpers %}
 {% load helpers %}
+{% load render_table from django_tables2 %}
 
 
 {% block title %}Assign an IP Address{% endblock title %}
 {% block title %}Assign an IP Address{% endblock title %}
 
 
@@ -35,7 +36,9 @@
         <div class="row mb-3">
         <div class="row mb-3">
             <div class="col col-md-12">
             <div class="col col-md-12">
                 <h3>Search Results</h3>
                 <h3>Search Results</h3>
-                {% include 'utilities/obj_table.html' %}
+                <div class="table-responsive">
+                  {% render_table table 'inc/table.html' %}
+                </div>
             </div>
             </div>
         </div>
         </div>
     {% endif %}
     {% endif %}

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

@@ -1,4 +1,5 @@
 {% extends 'ipam/iprange/base.html' %}
 {% extends 'ipam/iprange/base.html' %}
+{% load helpers %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
   {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
@@ -9,9 +10,30 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'utilities/obj_table.html' with table=ip_table heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_ipaddress %}
+          <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+          </button>
+        {% endif %}
+        {% if perms.ipam.delete_ipaddress %}
+          <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:iprange_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
+            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+          </button>
+        {% endif %}
+      </div>
     </div>
     </div>
-  </div>
+  </form>
+  {% table_config_form table %}
 {% endblock %}
 {% endblock %}

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

@@ -1,6 +1,5 @@
 {% extends 'ipam/prefix/base.html' %}
 {% extends 'ipam/prefix/base.html' %}
 {% load helpers %}
 {% load helpers %}
-{% load static %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and first_available_ip %}
   {% if perms.ipam.add_ipaddress and first_available_ip %}
@@ -11,11 +10,30 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'inc/table_controls.html' with table_modal="IPAddressTable_config" %}
-      {% include 'utilities/obj_table.html' with heading='IP Addresses' bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="IPAddressTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_ipaddress %}
+          <button type="submit" name="_edit" formaction="{% url 'ipam:ipaddress_bulk_edit' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+          </button>
+        {% endif %}
+        {% if perms.ipam.delete_ipaddress %}
+          <button type="submit" name="_delete" formaction="{% url 'ipam:ipaddress_bulk_delete' %}?return_url={% url 'ipam:prefix_ipaddresses' pk=object.pk %}" class="btn btn-danger btn-sm">
+            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+          </button>
+        {% endif %}
+      </div>
     </div>
     </div>
-  </div>
-  {% table_config_form table table_name="IPAddressTable" %}
+  </form>
+  {% table_config_form table %}
 {% endblock %}
 {% endblock %}

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

@@ -1,13 +1,31 @@
 {% extends 'ipam/prefix/base.html' %}
 {% extends 'ipam/prefix/base.html' %}
 {% load helpers %}
 {% load helpers %}
-{% load static %}
 
 
 {% block content %}
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'inc/table_controls.html' with table_modal="IPRangeTable_config" %}
-      {% include 'utilities/obj_table.html' with heading='Child IP Ranges' bulk_edit_url='ipam:iprange_bulk_edit' bulk_delete_url='ipam:iprange_bulk_delete' parent=prefix %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="IPRangeTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_iprange %}
+          <button type="submit" name="_edit" formaction="{% url 'ipam:iprange_bulk_edit' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+          </button>
+        {% endif %}
+        {% if perms.ipam.delete_iprange %}
+          <button type="submit" name="_delete" formaction="{% url 'ipam:iprange_bulk_delete' %}?return_url={% url 'ipam:prefix_ipranges' pk=object.pk %}" class="btn btn-danger btn-sm">
+            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+          </button>
+        {% endif %}
+      </div>
     </div>
     </div>
-  </div>
-  {% table_config_form table table_name="IPRangeTable" %}
+  </form>
+  {% table_config_form table %}
 {% endblock %}
 {% endblock %}

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

@@ -1,6 +1,5 @@
 {% extends 'ipam/prefix/base.html' %}
 {% extends 'ipam/prefix/base.html' %}
 {% load helpers %}
 {% load helpers %}
-{% load static %}
 
 
 {% block extra_controls %}
 {% block extra_controls %}
   {% include 'ipam/inc/toggle_available.html' %}
   {% include 'ipam/inc/toggle_available.html' %}
@@ -13,11 +12,30 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
-  <div class="row">
-    <div class="col col-md-12">
-      {% include 'inc/table_controls.html' with table_modal="PrefixTable_config" %}
-      {% include 'utilities/obj_table.html' with heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="PrefixTable_config" %}
+
+    <div class="card">
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.ipam.change_prefix %}
+          <button type="submit" name="_edit" formaction="{% url 'ipam:prefix_bulk_edit' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+          </button>
+        {% endif %}
+        {% if perms.ipam.delete_prefix %}
+          <button type="submit" name="_delete" formaction="{% url 'ipam:prefix_bulk_delete' %}?return_url={% url 'ipam:prefix_prefixes' pk=object.pk %}" class="btn btn-danger btn-sm">
+            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+          </button>
+        {% endif %}
+      </div>
     </div>
     </div>
-  </div>
-  {% table_config_form table table_name="PrefixTable" %}
+  </form>
+  {% table_config_form table %}
 {% endblock %}
 {% endblock %}

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

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.ipam.add_aggregate %}
+    <a href="{% url 'ipam:aggregate_add' %}?rir={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Aggregate
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -49,21 +58,12 @@
 <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">
-        Aggregates
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=aggregates_table %}
+      <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>
-      {% if perms.ipam.add_aggregate %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'ipam:aggregate_add' %}?rir={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Aggregate
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=aggregates_table.paginator page=aggregates_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

+ 13 - 13
netbox/templates/ipam/role.html

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.ipam.add_prefix %}
+    <a href="{% url 'ipam:prefix_add' %}?role={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Prefix
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -43,21 +52,12 @@
 <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">
-        Prefixes
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=prefixes_table %}
+      <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>
-      {% if perms.ipam.add_prefix %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'ipam:prefix_add' %}?role={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Prefix
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=prefixes_table.paginator page=prefixes_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

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

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

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

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

+ 6 - 17
netbox/templates/ipam/vlangroup.html

@@ -11,13 +11,12 @@
   {% endif %}
   {% endif %}
 {% endblock %}
 {% endblock %}
 
 
-{% block buttons %}
+{% block extra_controls %}
   {% if perms.ipam.add_vlan %}
   {% if perms.ipam.add_vlan %}
-    <a href="{% url 'ipam:vlan_add' %}?group={{ object.pk }}" class="btn btn-success">
+    <a href="{% url 'ipam:vlan_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
     </a>
     </a>
   {% endif %}
   {% endif %}
-  {{ block.super }}
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
@@ -66,22 +65,12 @@
 <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">
-        VLANs
-      </h5>
-      <div class="card-body">
+      <h5 class="card-header">VLANs</h5>
+      <div class="card-body table-responsive">
         {% render_table vlans_table 'inc/table.html' %}
         {% render_table vlans_table 'inc/table.html' %}
+        {% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
       </div>
       </div>
-      {% if perms.ipam.add_vlan %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'ipam:vlan_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add VLAN
-          </a>
-        </div>
-      {% endif %}
-    </div>
-    {% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
     </div>
     </div>
+  </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
-

+ 4 - 8
netbox/templates/tenancy/contact.html

@@ -75,19 +75,15 @@
       {% plugin_right_page object %}
       {% plugin_right_page object %}
     </div>
     </div>
   </div>
   </div>
-  <div class="row">
+  <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">Assignments</h5>
         <h5 class="card-header">Assignments</h5>
-        <div class="card-body">
-          {% if assignments_table.rows %}
-            {% render_table assignments_table 'inc/table.html' %}
-          {% else %}
-            <div class="text-muted">None</div>
-          {% endif %}
+        <div class="card-body table-responsive">
+          {% render_table assignments_table 'inc/table.html' %}
+          {% include 'inc/paginator.html' with paginator=assignments_table.paginator page=assignments_table.page %}
         </div>
         </div>
       </div>
       </div>
-      {% include 'inc/paginator.html' with paginator=assignments_table.paginator page=assignments_table.page %}
       {% plugin_full_width_page object %}
       {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>

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

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -54,8 +55,8 @@
         <h5 class="card-header">
         <h5 class="card-header">
           Child Groups
           Child Groups
         </h5>
         </h5>
-        <div class="card-body">
-          {% include 'inc/table.html' with table=child_groups_table %}
+        <div class="card-body table-responsive">
+          {% render_table child_groups_table 'inc/table.html' %}
         </div>
         </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">
@@ -71,22 +72,13 @@
   <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">
-          Contacts
-        </h5>
-        <div class="card-body">
-          {% include 'inc/table.html' with table=contacts_table %}
-        </div>
-        {% if perms.tenancy.add_contact %}
-          <div class="card-footer text-end noprint">
-            <a href="{% url 'tenancy:contact_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
-              <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Contact
-            </a>
-          </div>
-        {% endif %}
+        <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>
-        {% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
-        {% plugin_full_width_page object %}
+      </div>
+      {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>
 {% endblock %}
 {% endblock %}

+ 4 - 3
netbox/templates/tenancy/contactrole.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   <li class="breadcrumb-item"><a href="{% url 'tenancy:contactrole_list' %}">Contact Roles</a></li>
   <li class="breadcrumb-item"><a href="{% url 'tenancy:contactrole_list' %}">Contact Roles</a></li>
@@ -42,11 +43,11 @@
     <div class="col col-md-12">
     <div class="col col-md-12">
       <div class="card">
       <div class="card">
         <h5 class="card-header">Assigned Contacts</h5>
         <h5 class="card-header">Assigned Contacts</h5>
-        <div class="card-body">
-          {% include 'inc/table.html' with table=contacts_table %}
+        <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>
       </div>
       </div>
-      {% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
       {% plugin_full_width_page object %}
       {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>

+ 15 - 15
netbox/templates/tenancy/tenantgroup.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,6 +10,14 @@
   {% endfor %}
   {% endfor %}
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.tenancy.add_tenant %}
+    <a href="{% url 'tenancy:tenant_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Tenant
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -56,22 +65,13 @@
 <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">
-        Tenants
-      </div>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=tenants_table %}
+      <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>
-      {% if perms.tenancy.add_tenant %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'tenancy:tenant_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Tenant
-          </a>
-        </div>
-      {% endif %}
-      </div>
-      {% include 'inc/paginator.html' with paginator=tenants_table.paginator page=tenants_table.page %}
-      {% plugin_full_width_page object %}
+    </div>
+    {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 0 - 62
netbox/templates/utilities/obj_table.html

@@ -1,62 +0,0 @@
-{% load helpers %}
-{% load render_table from django_tables2 %}
-
-{% if permissions.change or permissions.delete %}
-    <form method="post" class="form form-horizontal">
-        {% csrf_token %}
-        <input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
-
-        {% if table.paginator.num_pages > 1 %}
-            <div id="select-all-box" class="d-none card noprint">
-              <div class="card-body">
-                <div class="float-end">
-                    {% if bulk_edit_url and permissions.change %}
-                        <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm" disabled="disabled">
-                            <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit All
-                        </button>
-                    {% endif %}
-                    {% if bulk_delete_url and permissions.delete %}
-                        <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if bulk_querystring %}?{{ bulk_querystring }}{% elif request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm" disabled="disabled">
-                            <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Delete All
-                        </button>
-                    {% endif %}
-                </div>
-                <div class="form-check">
-                    <input type="checkbox" id="select-all" name="_all" class="form-check-input" />
-                    <label for="select-all" class="form-check-label">
-                    Select <strong>all {{ table.objects_count }} {{ table.data.verbose_name_plural }}</strong> matching query
-                    </label>
-                </div>
-              </div>
-            </div>
-        {% endif %}
-
-        <div class="table-responsive">
-          {% render_table table 'inc/table.html' %}
-        </div>
-
-        <div class="float-start noprint">
-            {% block extra_actions %}{% endblock %}
-
-            {% if bulk_edit_url and permissions.change %}
-                <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
-                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
-                </button>
-            {% endif %}
-
-            {% if bulk_delete_url and permissions.delete %}
-                <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
-                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
-                </button>
-            {% endif %}
-        </div>
-    </form>
-{% else %}
-
-    <div class="table-responsive">
-      {% render_table table 'inc/table.html' %}
-    </div>
-
-{% endif %}
-
-{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}

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

@@ -3,26 +3,25 @@
 {% load render_table from django_tables2 %}
 {% load render_table from django_tables2 %}
 
 
 {% block content %}
 {% block content %}
-<div class="row">
-  <div class="col col-md-12">
+  <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="DeviceTable_config" %}
+
     <div class="card">
     <div class="card">
-      <h5 class="card-header">
-        Host Devices
-      </h5>
-      <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
-      {% csrf_token %}
-      <div class="card-body table-responsive">
-        {% render_table devices_table 'inc/table.html' %}
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
       </div>
       </div>
-      {% if perms.virtualization.change_cluster %}
-        <div class="card-footer noprint">
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.virtualization.change_cluster %}
           <button type="submit" name="_remove" class="btn btn-danger btn-sm">
           <button type="submit" name="_remove" class="btn btn-danger btn-sm">
             <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove Devices
             <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Remove Devices
           </button>
           </button>
-        </div>
-      {% endif %}
-      </form>
+        {% endif %}
+      </div>
     </div>
     </div>
-  </div>
-</div>
+  </form>
+  {% table_config_form table %}
 {% endblock %}
 {% endblock %}

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

@@ -3,16 +3,30 @@
 {% load render_table from django_tables2 %}
 {% load render_table from django_tables2 %}
 
 
 {% block content %}
 {% block content %}
-<div class="row">
-  <div class="col col-md-12">
+  <form method="post">
+    {% csrf_token %}
+    {% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineTable_config" %}
+
     <div class="card">
     <div class="card">
-      <h5 class="card-header">
-        Virtual Machines
-      </h5>
-      <div class="card-body table-responsive">
-        {% render_table virtualmachines_table 'inc/table.html' %}
+      <div class="card-body" id="object_list">
+        {% include 'htmx/table.html' %}
+      </div>
+    </div>
+
+    <div class="noprint bulk-buttons">
+      <div class="bulk-button-group">
+        {% if perms.virtualization.change_virtualmachine %}
+          <button type="submit" name="_edit" formaction="{% url 'virtualization:virtualmachine_bulk_edit' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-warning btn-sm">
+            <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+          </button>
+        {% endif %}
+        {% if perms.virtualization.delete_virtualmachine %}
+          <button type="submit" name="_delete" formaction="{% url 'virtualization:virtualmachine_bulk_delete' %}?return_url={% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="btn btn-danger btn-sm">
+            <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+          </button>
+        {% endif %}
       </div>
       </div>
     </div>
     </div>
-  </div>
-</div>
+  </form>
+  {% table_config_form table %}
 {% endblock %}
 {% endblock %}

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

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.virtualization.add_cluster %}
+    <a href="{% url 'virtualization:cluster_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -40,21 +49,12 @@
 <div class="row">
 <div class="row">
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
-      <h5 class="card-header">
-        Clusters
-      </h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=clusters_table %}
+      <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>
-      {% if perms.virtualization.add_cluster %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'virtualization:cluster_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

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

@@ -1,6 +1,15 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
+
+{% block extra_controls %}
+  {% if perms.virtualization.add_cluster %}
+    <a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
 
 
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
@@ -39,21 +48,12 @@
 <div class="row">
 <div class="row">
 	<div class="col col-md-12">
 	<div class="col col-md-12">
     <div class="card">
     <div class="card">
-      <h5 class="card-header">
-        Clusters
-      </h5>
-      <div class="card-body">
-      {% include 'inc/table.html' with table=clusters_table %}
+      <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>
-      {% if perms.virtualization.add_cluster %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster
-          </a>
-        </div>
-      {% endif %}
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

+ 18 - 18
netbox/templates/virtualization/virtualmachine.html

@@ -59,31 +59,31 @@
                     <tr>
                     <tr>
                         <th scope="row">Primary IPv4</th>
                         <th scope="row">Primary IPv4</th>
                         <td>
                         <td>
-                            {% if object.primary_ip4 %}
-                                <a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
-                                {% if object.primary_ip4.nat_inside %}
-                                    <span>(NAT for {{ object.primary_ip4.nat_inside.address.ip }})</span>
-                                {% elif object.primary_ip4.nat_outside %}
-                                    <span>(NAT: {{ object.primary_ip4.nat_outside.address.ip }})</span>
-                                {% endif %}
-                            {% else %}
-                                <span class="text-muted">&mdash;</span>
+                          {% if object.primary_ip4 %}
+                            <a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
+                            {% if object.primary_ip4.nat_inside %}
+                              (NAT for <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
+                            {% elif object.primary_ip4.nat_outside %}
+                              (NAT: <a href="{{ object.primary_ip4.nat_outside.get_absolute_url }}">{{ object.primary_ip4.nat_outside.address.ip }}</a>)
                             {% endif %}
                             {% endif %}
+                          {% else %}
+                            <span class="text-muted">&mdash;</span>
+                          {% endif %}
                         </td>
                         </td>
                     </tr>
                     </tr>
                     <tr>
                     <tr>
                         <th scope="row">Primary IPv6</th>
                         <th scope="row">Primary IPv6</th>
                         <td>
                         <td>
-                            {% if object.primary_ip6 %}
-                                <a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
-                                {% if object.primary_ip6.nat_inside %}
-                                    <span>(NAT for {{ object.primary_ip6.nat_inside.address.ip }})</span>
-                                {% elif object.primary_ip6.nat_outside %}
-                                    <span>(NAT: {{ object.primary_ip6.nat_outside.address.ip }})</span>
-                                {% endif %}
-                            {% else %}
-                                <span class="text-muted">&mdash;</span>
+                          {% if object.primary_ip6 %}
+                            <a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
+                            {% if object.primary_ip6.nat_inside %}
+                              (NAT for <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
+                            {% elif object.primary_ip6.nat_outside %}
+                              (NAT: <a href="{{ object.primary_ip6.nat_outside.get_absolute_url }}">{{ object.primary_ip6.nat_outside.address.ip }}</a>)
                             {% endif %}
                             {% endif %}
+                          {% else %}
+                            <span class="text-muted">&mdash;</span>
+                          {% endif %}
                         </td>
                         </td>
                     </tr>
                     </tr>
                 </table>
                 </table>

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

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

+ 1 - 1
netbox/templates/virtualization/vminterface.html

@@ -91,7 +91,7 @@
             <h5 class="card-header">
             <h5 class="card-header">
                 IP Addresses
                 IP Addresses
             </h5>
             </h5>
-            <div class="card-body">
+            <div class="card-body table-responsive">
                 {% if ipaddress_table.rows %}
                 {% if ipaddress_table.rows %}
                     {% render_table ipaddress_table 'inc/table.html' %}
                     {% render_table ipaddress_table 'inc/table.html' %}
                 {% else %}
                 {% else %}

+ 4 - 3
netbox/templates/wireless/wirelesslan.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block content %}
 {% block content %}
 <div class="row">
 <div class="row">
@@ -53,11 +54,11 @@
   <div class="col col-md-12">
   <div class="col col-md-12">
     <div class="card">
     <div class="card">
       <h5 class="card-header">Attached Interfaces</h5>
       <h5 class="card-header">Attached Interfaces</h5>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=interfaces_table %}
+      <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>
     </div>
     </div>
-    {% include 'inc/paginator.html' with paginator=interfaces_table.paginator page=interfaces_table.page %}
     {% plugin_full_width_page object %}
     {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>

+ 14 - 12
netbox/templates/wireless/wirelesslangroup.html

@@ -1,6 +1,7 @@
 {% extends 'generic/object.html' %}
 {% extends 'generic/object.html' %}
 {% load helpers %}
 {% load helpers %}
 {% load plugins %}
 {% load plugins %}
+{% load render_table from django_tables2 %}
 
 
 {% block breadcrumbs %}
 {% block breadcrumbs %}
   {{ block.super }}
   {{ block.super }}
@@ -9,6 +10,14 @@
   {% endfor %}
   {% endfor %}
 {% endblock %}
 {% endblock %}
 
 
+{% block extra_controls %}
+  {% if perms.wireless.add_wirelesslan %}
+    <a href="{% url 'wireless:wirelesslan_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
+      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Wireless LAN
+    </a>
+  {% endif %}
+{% endblock extra_controls %}
+
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<div class="col col-md-6">
@@ -55,19 +64,12 @@
 	<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-header">Wireless LANs</div>
-      <div class="card-body">
-        {% include 'inc/table.html' with table=wirelesslans_table %}
+      <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>
       </div>
-      {% if perms.wireless.add_wirelesslan %}
-        <div class="card-footer text-end noprint">
-          <a href="{% url 'wireless:wirelesslan_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
-            <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Wireless LAN
-          </a>
-        </div>
-      {% endif %}
-      </div>
-      {% include 'inc/paginator.html' with paginator=wirelesslans_table.paginator page=wirelesslans_table.page %}
-      {% plugin_full_width_page object %}
+    </div>
+    {% plugin_full_width_page object %}
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 5 - 0
netbox/utilities/htmx.py

@@ -0,0 +1,5 @@
+def is_htmx(request):
+    """
+    Returns True if the request was made by HTMX; False otherwise.
+    """
+    return 'Hx-Request' in request.headers

+ 1 - 0
netbox/virtualization/tables.py

@@ -114,6 +114,7 @@ class ClusterTable(BaseTable):
 class VirtualMachineTable(BaseTable):
 class VirtualMachineTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     name = tables.Column(
     name = tables.Column(
+        order_by=('_name',),
         linkify=True
         linkify=True
     )
     )
     status = ChoiceFieldColumn()
     status = ChoiceFieldColumn()

+ 22 - 30
netbox/virtualization/views.py

@@ -4,6 +4,7 @@ from django.db.models import Prefetch
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 
 
+from dcim.filtersets import DeviceFilterSet
 from dcim.models import Device
 from dcim.models import Device
 from dcim.tables import DeviceTable
 from dcim.tables import DeviceTable
 from extras.views import ObjectConfigContextView
 from extras.views import ObjectConfigContextView
@@ -161,38 +162,34 @@ class ClusterView(generic.ObjectView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
 
 
 
 
-class ClusterVirtualMachinesView(generic.ObjectView):
+class ClusterVirtualMachinesView(generic.ObjectChildrenView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
+    child_model = VirtualMachine
+    table = tables.VirtualMachineTable
+    filterset = filtersets.VirtualMachineFilterSet
     template_name = 'virtualization/cluster/virtual_machines.html'
     template_name = 'virtualization/cluster/virtual_machines.html'
 
 
-    def get_extra_context(self, request, instance):
-        virtualmachines = VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=instance)
-        virtualmachines_table = tables.VirtualMachineTable(
-            virtualmachines,
-            exclude=('cluster',),
-            orderable=False
-        )
+    def get_children(self, request, parent):
+        return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent)
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'virtualmachines_table': virtualmachines_table,
             'active_tab': 'virtual-machines',
             'active_tab': 'virtual-machines',
         }
         }
 
 
 
 
-class ClusterDevicesView(generic.ObjectView):
+class ClusterDevicesView(generic.ObjectChildrenView):
     queryset = Cluster.objects.all()
     queryset = Cluster.objects.all()
+    child_model = Device
+    table = DeviceTable
+    filterset = DeviceFilterSet
     template_name = 'virtualization/cluster/devices.html'
     template_name = 'virtualization/cluster/devices.html'
 
 
-    def get_extra_context(self, request, instance):
-        devices = Device.objects.restrict(request.user, 'view').filter(cluster=instance).prefetch_related(
-            'site', 'rack', 'tenant', 'device_type__manufacturer'
-        )
-        devices_table = DeviceTable(list(devices), orderable=False)
-        if request.user.has_perm('virtualization.change_cluster'):
-            devices_table.columns.show('pk')
+    def get_children(self, request, parent):
+        return Device.objects.restrict(request.user, 'view').filter(cluster=parent)
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'devices_table': devices_table,
             'active_tab': 'devices',
             'active_tab': 'devices',
         }
         }
 
 
@@ -347,26 +344,21 @@ class VirtualMachineView(generic.ObjectView):
         }
         }
 
 
 
 
-class VirtualMachineInterfacesView(generic.ObjectView):
+class VirtualMachineInterfacesView(generic.ObjectChildrenView):
     queryset = VirtualMachine.objects.all()
     queryset = VirtualMachine.objects.all()
+    child_model = VMInterface
+    table = tables.VMInterfaceTable
+    filterset = filtersets.VMInterfaceFilterSet
     template_name = 'virtualization/virtualmachine/interfaces.html'
     template_name = 'virtualization/virtualmachine/interfaces.html'
 
 
-    def get_extra_context(self, request, instance):
-        interfaces = instance.interfaces.restrict(request.user, 'view').prefetch_related(
+    def get_children(self, request, parent):
+        return parent.interfaces.restrict(request.user, 'view').prefetch_related(
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
             'tags',
             'tags',
         )
         )
-        interface_table = tables.VirtualMachineVMInterfaceTable(
-            data=interfaces,
-            user=request.user,
-            orderable=False
-        )
-        if request.user.has_perm('virtualization.change_vminterface') or \
-                request.user.has_perm('virtualization.delete_vminterface'):
-            interface_table.columns.show('pk')
 
 
+    def get_extra_context(self, request, instance):
         return {
         return {
-            'interface_table': interface_table,
             'active_tab': 'interfaces',
             'active_tab': 'interfaces',
         }
         }
 
 

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor