diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 0224b9c15..f03f23a3e 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -1,5 +1,23 @@ # 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) ### Enhancements diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 6fb3e2a00..4ec31e60c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -36,26 +36,15 @@ from .models import ( ) -class DeviceComponentsView(generic.ObjectView): +class DeviceComponentsView(generic.ObjectChildrenView): 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): - 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 { - '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() 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): @@ -806,43 +795,51 @@ class DeviceTypeView(generic.ObjectView): class DeviceTypeConsolePortsView(DeviceTypeComponentsView): - model = ConsolePortTemplate + child_model = ConsolePortTemplate table = tables.ConsolePortTemplateTable + filterset = filtersets.ConsolePortTemplateFilterSet class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView): - model = ConsoleServerPortTemplate + child_model = ConsoleServerPortTemplate table = tables.ConsoleServerPortTemplateTable + filterset = filtersets.ConsoleServerPortTemplateFilterSet class DeviceTypePowerPortsView(DeviceTypeComponentsView): - model = PowerPortTemplate + child_model = PowerPortTemplate table = tables.PowerPortTemplateTable + filterset = filtersets.PowerPortTemplateFilterSet class DeviceTypePowerOutletsView(DeviceTypeComponentsView): - model = PowerOutletTemplate + child_model = PowerOutletTemplate table = tables.PowerOutletTemplateTable + filterset = filtersets.PowerOutletTemplateFilterSet class DeviceTypeInterfacesView(DeviceTypeComponentsView): - model = InterfaceTemplate + child_model = InterfaceTemplate table = tables.InterfaceTemplateTable + filterset = filtersets.InterfaceTemplateFilterSet class DeviceTypeFrontPortsView(DeviceTypeComponentsView): - model = FrontPortTemplate + child_model = FrontPortTemplate table = tables.FrontPortTemplateTable + filterset = filtersets.FrontPortTemplateFilterSet class DeviceTypeRearPortsView(DeviceTypeComponentsView): - model = RearPortTemplate + child_model = RearPortTemplate table = tables.RearPortTemplateTable + filterset = filtersets.RearPortTemplateFilterSet class DeviceTypeDeviceBaysView(DeviceTypeComponentsView): - model = DeviceBayTemplate + child_model = DeviceBayTemplate table = tables.DeviceBayTemplateTable + filterset = filtersets.DeviceBayTemplateFilterSet class DeviceTypeEditView(generic.ObjectEditView): @@ -1337,62 +1334,71 @@ class DeviceView(generic.ObjectView): class DeviceConsolePortsView(DeviceComponentsView): - model = ConsolePort + child_model = ConsolePort table = tables.DeviceConsolePortTable + filterset = filtersets.ConsolePortFilterSet template_name = 'dcim/device/consoleports.html' class DeviceConsoleServerPortsView(DeviceComponentsView): - model = ConsoleServerPort + child_model = ConsoleServerPort table = tables.DeviceConsoleServerPortTable + filterset = filtersets.ConsoleServerPortFilterSet template_name = 'dcim/device/consoleserverports.html' class DevicePowerPortsView(DeviceComponentsView): - model = PowerPort + child_model = PowerPort table = tables.DevicePowerPortTable + filterset = filtersets.PowerPortFilterSet template_name = 'dcim/device/powerports.html' class DevicePowerOutletsView(DeviceComponentsView): - model = PowerOutlet + child_model = PowerOutlet table = tables.DevicePowerOutletTable + filterset = filtersets.PowerOutletFilterSet template_name = 'dcim/device/poweroutlets.html' class DeviceInterfacesView(DeviceComponentsView): - model = Interface + child_model = Interface table = tables.DeviceInterfaceTable + filterset = filtersets.InterfaceFilterSet 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('member_interfaces', queryset=Interface.objects.restrict(request.user)) ) class DeviceFrontPortsView(DeviceComponentsView): - model = FrontPort + child_model = FrontPort table = tables.DeviceFrontPortTable + filterset = filtersets.FrontPortFilterSet template_name = 'dcim/device/frontports.html' class DeviceRearPortsView(DeviceComponentsView): - model = RearPort + child_model = RearPort table = tables.DeviceRearPortTable + filterset = filtersets.RearPortFilterSet template_name = 'dcim/device/rearports.html' class DeviceDeviceBaysView(DeviceComponentsView): - model = DeviceBay + child_model = DeviceBay table = tables.DeviceDeviceBayTable + filterset = filtersets.DeviceBayFilterSet template_name = 'dcim/device/devicebays.html' class DeviceInventoryView(DeviceComponentsView): - model = InventoryItem + child_model = InventoryItem table = tables.DeviceInventoryItemTable + filterset = filtersets.InventoryItemFilterSet template_name = 'dcim/device/inventory.html' diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 89fd00929..1be187596 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -170,17 +170,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer): @swagger_serializer_method(serializer_or_field=serializers.DictField) 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 diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py index 17b292625..4c11d8821 100644 --- a/netbox/extras/management/commands/nbshell.py +++ b/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.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}) ### Python {python} | Django {django} | NetBox {netbox} diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index be5ae6416..59d16fff8 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -22,7 +22,7 @@ class ConfigContextQuerySet(RestrictedQuerySet): # Device type assignment is relevant only for Devices 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_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 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 - 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): """ Attach the subquery annotation to the base queryset @@ -123,6 +120,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): elif self.model._meta.model_name == 'virtualmachine': 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(device_types=None), Q.AND) region_field = 'cluster__site__region' sitegroup_field = 'cluster__site__group' diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index c361acd01..aeb71e70f 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -195,6 +195,12 @@ class Aggregate(PrimaryModel): return self.prefix.version 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): """ Determine the prefix utilization of the aggregate and return it as a percentage. diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 541acb3ac..e9bba8fa1 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -61,6 +61,7 @@ urlpatterns = [ path('aggregates/edit/', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), path('aggregates/delete/', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), path('aggregates//', views.AggregateView.as_view(), name='aggregate'), + path('aggregates//prefixes/', views.AggregatePrefixesView.as_view(), name='aggregate_prefixes'), path('aggregates//edit/', views.AggregateEditView.as_view(), name='aggregate_edit'), path('aggregates//delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'), path('aggregates//changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}), diff --git a/netbox/ipam/utils.py b/netbox/ipam/utils.py index 0e79e7f78..97da9e4fe 100644 --- a/netbox/ipam/utils.py +++ b/netbox/ipam/utils.py @@ -4,20 +4,34 @@ from .constants import * 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 = [] - # 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 available prefixes to the table if requested + if prefix_list and show_available: - # Concatenate and sort complete list of children - prefix_list = list(prefix_list) + available_prefixes - prefix_list.sort(key=lambda p: p.prefix) + # 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 - return prefix_list + # Add assigned prefixes to the table if requested + if prefix_list and show_assigned: + child_prefixes = child_prefixes + list(prefix_list) + + # Sort child prefixes after additions + child_prefixes.sort(key=lambda p: p.prefix) + + return child_prefixes def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6d3963599..34ad39df6 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,21 +1,22 @@ from django.contrib.contenttypes.models import ContentType from django.db.models import Prefetch from django.db.models.expressions import RawSQL -from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from dcim.filtersets import InterfaceFilterSet from dcim.models import Device, Interface, Site from dcim.tables import SiteTable from netbox.views import generic from utilities.tables import paginate_table from utilities.utils import count_related +from virtualization.filtersets import VMInterfaceFilterSet from virtualization.models import VirtualMachine, VMInterface from . import filtersets, forms, tables from .constants import * from .models import * from .models import ASN -from .utils import add_available_ipaddresses, add_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): queryset = Aggregate.objects.all() + +class AggregatePrefixesView(generic.ObjectChildrenView): + queryset = Aggregate.objects.all() + child_model = Prefix + table = tables.PrefixTable + filterset = filtersets.PrefixFilterSet + template_name = 'ipam/aggregate/prefixes.html' + + 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') + + 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): - # 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) - - 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) - - # 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'), - } - return { - 'prefix_table': prefix_table, - 'permissions': permissions, '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() + child_model = Prefix + table = tables.PrefixTable + filterset = filtersets.PrefixFilterSet template_name = 'ipam/prefix/prefixes.html' + def get_children(self, request, parent): + return parent.get_child_prefixes().restrict(request.user, 'view') + + 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): - # 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) - - 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) - - 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 { - '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', '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() + child_model = IPRange + table = tables.IPRangeTable + filterset = filtersets.IPRangeFilterSet template_name = 'ipam/prefix/ip_ranges.html' + def get_children(self, request, parent): + return parent.get_child_ranges().restrict(request.user, 'view') + 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'), - } - 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', } -class PrefixIPAddressesView(generic.ObjectView): +class PrefixIPAddressesView(generic.ObjectChildrenView): queryset = Prefix.objects.all() + child_model = IPAddress + table = tables.IPAddressTable + filterset = filtersets.IPAddressFilterSet template_name = 'ipam/prefix/ip_addresses.html' + def get_children(self, request, parent): + return parent.get_child_ips().restrict(request.user, 'view') + 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'), - } - 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', '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() -class IPRangeIPAddressesView(generic.ObjectView): +class IPRangeIPAddressesView(generic.ObjectChildrenView): queryset = IPRange.objects.all() + child_model = IPAddress + table = tables.IPAddressTable + filterset = filtersets.IPAddressFilterSet template_name = 'ipam/iprange/ip_addresses.html' + def get_children(self, request, parent): + return parent.get_child_ips().restrict(request.user, 'view') + 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'), - } - return { - 'ip_table': ip_table, - 'permissions': permissions, '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() + child_model = Interface + table = tables.VLANDevicesTable + filterset = InterfaceFilterSet 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 { - 'members_table': members_table, 'active_tab': 'interfaces', } -class VLANVMInterfacesView(generic.ObjectView): +class VLANVMInterfacesView(generic.ObjectChildrenView): queryset = VLAN.objects.all() + child_model = VMInterface + table = tables.VLANVirtualMachinesTable + filterset = VMInterfaceFilterSet 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 { - 'members_table': members_table, 'active_tab': 'vminterfaces', } diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 83eabdb23..c6f6305cb 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -23,6 +23,7 @@ from utilities.exceptions import AbortTransaction, PermissionsViolation from utilities.forms import ( 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.tables import paginate_table 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): """ List a series of objects. @@ -208,6 +278,12 @@ class ObjectListView(ObjectPermissionRequiredMixin, View): table = self.get_table(request, permissions) 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 = { 'content_type': content_type, 'table': table, diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 4def5c73e..25017505e 100644 Binary files a/netbox/project-static/dist/netbox-dark.css and b/netbox/project-static/dist/netbox-dark.css differ diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index fa8a82cc4..07ad0dba2 100644 Binary files a/netbox/project-static/dist/netbox-light.css and b/netbox/project-static/dist/netbox-light.css differ diff --git a/netbox/project-static/dist/netbox-print.css b/netbox/project-static/dist/netbox-print.css index 3fda1a026..a09f49222 100644 Binary files a/netbox/project-static/dist/netbox-print.css and b/netbox/project-static/dist/netbox-print.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index c4a8e802d..95fd99270 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index b95e6ba02..6fbe0874b 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index 6b046c8fc..4ec08d7b3 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -30,6 +30,7 @@ "cookie": "^0.4.1", "dayjs": "^1.10.4", "flatpickr": "4.6.3", + "htmx.org": "^1.6.1", "just-debounce-it": "^1.4.0", "masonry-layout": "^4.2.2", "query-string": "^6.14.1", diff --git a/netbox/project-static/src/buttons/index.ts b/netbox/project-static/src/buttons/index.ts index 1fcc7b87e..251e0feaf 100644 --- a/netbox/project-static/src/buttons/index.ts +++ b/netbox/project-static/src/buttons/index.ts @@ -1,7 +1,6 @@ import { initConnectionToggle } from './connectionToggle'; import { initDepthToggle } from './depthToggle'; import { initMoveButtons } from './moveOptions'; -import { initPerPage } from './pagination'; import { initPreferenceUpdate } from './preferences'; import { initReslug } from './reslug'; import { initSelectAll } from './selectAll'; @@ -13,7 +12,6 @@ export function initButtons(): void { initReslug, initSelectAll, initPreferenceUpdate, - initPerPage, initMoveButtons, ]) { func(); diff --git a/netbox/project-static/src/buttons/pagination.ts b/netbox/project-static/src/buttons/pagination.ts deleted file mode 100644 index 670dc7390..000000000 --- a/netbox/project-static/src/buttons/pagination.ts +++ /dev/null @@ -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('select.per-page')) { - element.addEventListener('change', handlePerPageSelect); - } -} diff --git a/netbox/project-static/src/index.ts b/netbox/project-static/src/index.ts index 30c25a406..266e69d6d 100644 --- a/netbox/project-static/src/index.ts +++ b/netbox/project-static/src/index.ts @@ -1,4 +1,5 @@ import '@popperjs/core'; import 'bootstrap'; +import 'htmx.org'; import 'simplebar'; import './netbox'; diff --git a/netbox/project-static/src/search.ts b/netbox/project-static/src/search.ts index 3140b8d7f..9e8a31c5b 100644 --- a/netbox/project-static/src/search.ts +++ b/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 @@ -41,109 +40,8 @@ function initSearchBar(): void { } } -/** - * Initialize Interface Table Filter Elements. - */ -function initInterfaceFilter(): void { - for (const input of getElements('input.interface-filter')) { - const table = findFirstAdjacent(input, 'table'); - const rows = Array.from( - table?.querySelectorAll('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('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('input.object-filter')) { - // Find the first adjacent table element. - const table = findFirstAdjacent(input, 'table'); - - // Build a valid array of elements that are children of the adjacent table. - const rows = Array.from( - table?.querySelectorAll('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 = []; - - 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('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 { - for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) { + for (const func of [initSearchBar]) { func(); } } diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 58bf18286..acbfa0646 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -737,10 +737,6 @@ nav.breadcrumb-container { } } -div.paginator > form > div.input-group { - width: fit-content; -} - label.required { 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 .nav-tabs { .nav-link { diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 9dfc0a3b3..780ba071e 100644 --- a/netbox/project-static/yarn.lock +++ b/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" 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: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" diff --git a/netbox/templates/circuits/circuittype.html b/netbox/templates/circuits/circuittype.html index 57737a6d1..c2ab235e4 100644 --- a/netbox/templates/circuits/circuittype.html +++ b/netbox/templates/circuits/circuittype.html @@ -1,6 +1,15 @@ {% extends 'generic/object.html' %} {% load helpers %} {% load plugins %} +{% load render_table from django_tables2 %} + +{% block extra_controls %} + {% if perms.circuits.add_circuit %} + + Add Circuit + + {% endif %} +{% endblock extra_controls %} {% block content %}
@@ -39,22 +48,13 @@
-
- Circuits -
-
- {% include 'inc/table.html' with table=circuits_table %} +
Circuits
+
+ {% render_table circuits_table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %}
- {% if perms.circuits.add_circuit %} - - {% endif %} -
- {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} - {% plugin_full_width_page object %} +
+ {% plugin_full_width_page object %}
{% endblock %} diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index c16afa421..b04e7be7a 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -2,9 +2,18 @@ {% load static %} {% load helpers %} {% load plugins %} +{% load render_table from django_tables2 %} + +{% block extra_controls %} + {% if perms.circuits.add_circuit %} + + Add circuit + + {% endif %} +{% endblock extra_controls %} {% block content %} -
+
@@ -56,28 +65,17 @@ {% include 'inc/panels/contacts.html' %} {% plugin_right_page object %}
-
-
-
- Circuits -
-
- {% include 'inc/table.html' with table=circuits_table %} -
- {% if perms.circuits.add_circuit %} - - {% endif %} -
- {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} -
-
-
- {% plugin_full_width_page object %} +
+
+
+
Circuits
+
+ {% render_table circuits_table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} +
+ {% plugin_full_width_page object %} +
{% endblock %} diff --git a/netbox/templates/circuits/providernetwork.html b/netbox/templates/circuits/providernetwork.html index 9641c9934..970cd4a54 100644 --- a/netbox/templates/circuits/providernetwork.html +++ b/netbox/templates/circuits/providernetwork.html @@ -2,6 +2,7 @@ {% load static %} {% load helpers %} {% load plugins %} +{% load render_table from django_tables2 %} {% block breadcrumbs %} {{ block.super }} @@ -9,7 +10,7 @@ {% endblock %} {% block content %} -
+
@@ -43,22 +44,16 @@ {% plugin_right_page object %}
-
-
-
-
- Circuits -
-
- {% include 'inc/table.html' with table=circuits_table %} -
-
+
+
+
+
Circuits
+
+ {% render_table circuits_table 'inc/table.html' %} {% include 'inc/paginator.html' with paginator=circuits_table.paginator page=circuits_table.page %} +
-
-
-
- {% plugin_full_width_page object %} -
+ {% plugin_full_width_page object %} +
{% endblock %} diff --git a/netbox/templates/dcim/connections_list.html b/netbox/templates/dcim/connections_list.html index 5dbea9129..ef8bef828 100644 --- a/netbox/templates/dcim/connections_list.html +++ b/netbox/templates/dcim/connections_list.html @@ -8,19 +8,14 @@ {% block content-wrapper %}
- {# Conncetions list #} + {# Connections list #}
- {% include 'inc/table_controls.html' %} - + {% include 'inc/table_controls_htmx.html' %}
-
-
- {% render_table table 'inc/table.html' %} -
+
+ {% include 'htmx/table.html' %}
- - {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{# Filter form #} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index ea0c795c5..abe9d4deb 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -179,31 +179,31 @@ Primary IPv4 - {% if object.primary_ip4 %} - {{ object.primary_ip4.address.ip }} - {% if object.primary_ip4.nat_inside %} - (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) - {% elif object.primary_ip4.nat_outside %} - (NAT: {{ object.primary_ip4.nat_outside.address.ip }}) - {% endif %} - {% else %} - + {% if object.primary_ip4 %} + {{ object.primary_ip4.address.ip }} + {% if object.primary_ip4.nat_inside %} + (NAT for {{ object.primary_ip4.nat_inside.address.ip }}) + {% elif object.primary_ip4.nat_outside %} + (NAT: {{ object.primary_ip4.nat_outside.address.ip }}) {% endif %} + {% else %} + + {% endif %} Primary IPv6 - {% if object.primary_ip6 %} - {{ object.primary_ip6.address.ip }} - {% if object.primary_ip6.nat_inside %} - (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) - {% elif object.primary_ip6.nat_outside %} - (NAT: {{ object.primary_ip6.nat_outside.address.ip }}) - {% endif %} - {% else %} - + {% if object.primary_ip6 %} + {{ object.primary_ip6.address.ip }} + {% if object.primary_ip6.nat_inside %} + (NAT for {{ object.primary_ip6.nat_inside.address.ip }}) + {% elif object.primary_ip6.nat_outside %} + (NAT: {{ object.primary_ip6.nat_outside.address.ip }}) {% endif %} + {% else %} + + {% endif %} {% if object.cluster %} diff --git a/netbox/templates/dcim/device/consoleports.html b/netbox/templates/dcim/device/consoleports.html index 6cf736523..b5a8a0f39 100644 --- a/netbox/templates/dcim/device/consoleports.html +++ b/netbox/templates/dcim/device/consoleports.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_consoleport %} @@ -36,6 +42,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/consoleserverports.html b/netbox/templates/dcim/device/consoleserverports.html index ca159029e..e4e044288 100644 --- a/netbox/templates/dcim/device/consoleserverports.html +++ b/netbox/templates/dcim/device/consoleserverports.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_consoleserverport %} @@ -36,6 +42,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/devicebays.html b/netbox/templates/dcim/device/devicebays.html index b72625005..2d66e860d 100644 --- a/netbox/templates/dcim/device/devicebays.html +++ b/netbox/templates/dcim/device/devicebays.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_devicebay %} @@ -33,6 +39,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/frontports.html b/netbox/templates/dcim/device/frontports.html index 5833a1c78..2caa4d86c 100644 --- a/netbox/templates/dcim/device/frontports.html +++ b/netbox/templates/dcim/device/frontports.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_frontport %} @@ -36,6 +42,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/interfaces.html b/netbox/templates/dcim/device/interfaces.html index 1d1e7e81b..a862573ad 100644 --- a/netbox/templates/dcim/device/interfaces.html +++ b/netbox/templates/dcim/device/interfaces.html @@ -9,7 +9,15 @@
- +
@@ -34,7 +42,13 @@
- {% render_table table 'inc/table.html' %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_interface %} @@ -63,6 +77,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/inventory.html b/netbox/templates/dcim/device/inventory.html index 2aad68984..345884bf9 100644 --- a/netbox/templates/dcim/device/inventory.html +++ b/netbox/templates/dcim/device/inventory.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_inventoryitem %} @@ -33,6 +39,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/lldp_neighbors.html b/netbox/templates/dcim/device/lldp_neighbors.html index b3b2eeae8..adf8efdce 100644 --- a/netbox/templates/dcim/device/lldp_neighbors.html +++ b/netbox/templates/dcim/device/lldp_neighbors.html @@ -31,12 +31,12 @@ {% for iface in interfaces %} - {{ iface }} + {{ iface }} {% if iface.connected_endpoint.device %} - + {{ iface.connected_endpoint.device }} - + {{ iface.connected_endpoint }} {% elif iface.connected_endpoint.circuit %} diff --git a/netbox/templates/dcim/device/poweroutlets.html b/netbox/templates/dcim/device/poweroutlets.html index df936742e..7b945438c 100644 --- a/netbox/templates/dcim/device/poweroutlets.html +++ b/netbox/templates/dcim/device/poweroutlets.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_powerport %} @@ -36,6 +42,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/powerports.html b/netbox/templates/dcim/device/powerports.html index 5a502dc57..6f1ce2040 100644 --- a/netbox/templates/dcim/device/powerports.html +++ b/netbox/templates/dcim/device/powerports.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_powerport %} @@ -36,6 +42,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/device/rearports.html b/netbox/templates/dcim/device/rearports.html index d0ff55ec9..12d5929c3 100644 --- a/netbox/templates/dcim/device/rearports.html +++ b/netbox/templates/dcim/device/rearports.html @@ -6,8 +6,14 @@ {% block content %}
{% 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" %} + +
+
+ {% include 'htmx/table.html' %} +
+
+
{% if perms.dcim.change_rearport %} @@ -36,6 +42,5 @@ {% endif %}
- {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} {% table_config_form table %} {% endblock %} diff --git a/netbox/templates/dcim/devicerole.html b/netbox/templates/dcim/devicerole.html index 22385ae27..4db2d3ad8 100644 --- a/netbox/templates/dcim/devicerole.html +++ b/netbox/templates/dcim/devicerole.html @@ -1,11 +1,20 @@ {% extends 'generic/object.html' %} {% load helpers %} {% load plugins %} +{% load render_table from django_tables2 %} {% block breadcrumbs %} {% endblock %} +{% block extra_controls %} + {% if perms.dcim.add_device %} + + Add Device + + {% endif %} +{% endblock extra_controls %} + {% block content %}
@@ -69,21 +78,12 @@
-
- Devices -
-
- {% include 'inc/table.html' with table=devices_table %} +
Devices
+
+ {% render_table devices_table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %}
- {% if perms.dcim.add_device %} - - {% endif %}
- {% include 'inc/paginator.html' with paginator=devices_table.paginator page=devices_table.page %} {% plugin_full_width_page object %}
diff --git a/netbox/templates/dcim/devicetype/component_templates.html b/netbox/templates/dcim/devicetype/component_templates.html index d83a232cd..b1e0daf78 100644 --- a/netbox/templates/dcim/devicetype/component_templates.html +++ b/netbox/templates/dcim/devicetype/component_templates.html @@ -7,11 +7,9 @@
{% csrf_token %}
-
- {{ title }} -
-
- {% render_table table 'inc/table.html' %} +
{{ title }}
+
+ {% include 'htmx/table.html' %}