Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2021-12-15 10:53:21 -05:00
commit 997e88af00
96 changed files with 1252 additions and 1054 deletions

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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}

View File

@ -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'

View File

@ -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.

View File

@ -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/<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>/delete/', views.AggregateDeleteView.as_view(), name='aggregate_delete'),
path('aggregates/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='aggregate_changelog', kwargs={'model': Aggregate}),

View File

@ -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):

View File

@ -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',
}

View File

@ -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,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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",

View File

@ -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();

View File

@ -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);
}
}

View File

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

View File

@ -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<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 {
for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) {
for (const func of [initSearchBar]) {
func();
}
}

View File

@ -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 {

View File

@ -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"

View File

@ -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 %}
<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 %}
<div class="row mb-3">
@ -39,22 +48,13 @@
<div class="row mb-3">
<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 %}
<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>
{% 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 %}
</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>
{% endblock %}

View File

@ -2,9 +2,18 @@
{% load static %}
{% load helpers %}
{% 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 %}
<div class="row">
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
@ -56,28 +65,17 @@
{% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %}
</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 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>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -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 %}
<div class="row">
<div class="row mb-3">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
@ -43,22 +44,16 @@
{% plugin_right_page object %}
</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 %}
</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>
{% endblock %}

View File

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

View File

@ -179,31 +179,31 @@
<tr>
<th scope="row">Primary IPv4</th>
<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 %}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Primary IPv6</th>
<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 %}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
{% if object.cluster %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_consoleport %}
@ -36,6 +42,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_consoleserverport %}
@ -36,6 +42,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_devicebay %}
@ -33,6 +39,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_frontport %}
@ -36,6 +42,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

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

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_inventoryitem %}
@ -33,6 +39,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -31,12 +31,12 @@
<tbody>
{% for iface in interfaces %}
<tr id="{{ iface.name }}">
<td class="font-monospace">{{ iface }}</td>
<td>{{ iface }}</td>
{% 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>
</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>
</td>
{% elif iface.connected_endpoint.circuit %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_powerport %}
@ -36,6 +42,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_powerport %}
@ -36,6 +42,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -6,8 +6,14 @@
{% block content %}
<form method="post">
{% 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="bulk-button-group">
{% if perms.dcim.change_rearport %}
@ -36,6 +42,5 @@
{% endif %}
</div>
</form>
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
{% table_config_form table %}
{% endblock %}

View File

@ -1,11 +1,20 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'dcim:devicerole_list' %}">Device Roles</a></li>
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -69,21 +78,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -9,6 +10,14 @@
{% endfor %}
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -88,22 +97,13 @@
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">
Locations
</h5>
<div class="card-body">
{% include 'inc/table.html' with table=child_locations_table %}
<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>
{% 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 %}
</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>
{% endblock %}

View File

@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% 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 %}
<div class="row mb-3">
@ -46,21 +55,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=devicetypes_table.paginator page=devicetypes_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -9,6 +10,14 @@
{% endif %}
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -74,22 +83,13 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

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

View File

@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% 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 %}
<div class="row mb-3">
@ -45,21 +54,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=racks_table.paginator page=racks_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -9,6 +10,14 @@
{% endfor %}
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -55,8 +64,8 @@
<h5 class="card-header">
Child Regions
</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>
{% if perms.dcim.add_region %}
<div class="card-footer text-end noprint">
@ -69,25 +78,16 @@
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% endblock %}

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -9,6 +10,14 @@
{% endfor %}
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -55,8 +64,8 @@
<h5 class="card-header">
Child Groups
</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>
{% if perms.dcim.add_sitegroup %}
<div class="card-footer text-end noprint">
@ -72,21 +81,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -2,9 +2,17 @@
{% load render_table from django_tables2 %}
{% 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 %}
</div>
</div>
</div>
{% endblock %}

View File

@ -24,6 +24,10 @@
</div>
</form>
{% 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 %}

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row">
@ -63,12 +64,18 @@
</table>
</div>
</div>
</div>
<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>
</div>
{% plugin_full_width_page object %}
<div class="row">
<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>
{% endblock %}

View File

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

View File

@ -1,5 +1,6 @@
{% extends 'base/layout.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}Delete {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
@ -15,7 +16,9 @@
</div>
</div>
<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">
<form action="" method="post">
{% csrf_token %}

View File

@ -1,6 +1,7 @@
{% extends 'base/layout.html' %}
{% load helpers %}
{% load form_helpers %}
{% load render_table from django_tables2 %}
{% block title %}Editing {{ table.rows|length }} {{ obj_type_plural|bettertitle }}{% endblock %}
@ -59,7 +60,9 @@
{# Selected objects list #}
<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>
{% endblock %}

View File

@ -1,5 +1,6 @@
{% extends 'base/layout.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% block title %}Remove {{ table.rows|length }} {{ obj_type_plural|bettertitle }}?{% endblock %}
@ -13,7 +14,9 @@
</div>
</div>
<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">
{% csrf_token %}
{% for field in form.hidden_fields %}

View File

@ -87,7 +87,7 @@
{% endif %}
{# 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">
{% csrf_token %}
@ -95,10 +95,8 @@
{# Object table #}
<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>
@ -125,8 +123,6 @@
</form>
{# Paginator #}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>
{# Filter form #}

View File

@ -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 %}

View File

@ -1,51 +1,52 @@
{% 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 %}
<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>
</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 %}
{% 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>
</a>
{% endif %}
</div>
{% 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 %}
{% 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 %}
<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>
{% if page %}
<small class="text-end text-muted">
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
</small>
{% 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>
</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="{% 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 }}
</small>
{% endif %}
</div>
</div>

View File

@ -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>

View File

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

View File

@ -1,43 +1,41 @@
{% 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>
{% 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>
</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>

View File

@ -1,11 +1,16 @@
{% load helpers %}
<div class="row mb-3 justify-content-between">
<div class="table-controls noprint col col-12 col-md-8 col-lg-4">
<div class="input-group input-group-sm">
<input
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>

View File

@ -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>

View File

@ -1,82 +1,66 @@
{% extends 'generic/object.html' %}
{% extends 'ipam/aggregate/base.html' %}
{% load buttons %}
{% load helpers %}
{% 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 %}
<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>
{% plugin_left_page object %}
</div>
{% plugin_left_page object %}
</div>
<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 class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</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>
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -64,7 +64,7 @@
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">Virtual IP Addresses</h5>
<div class="card-body">
<div class="card-body table-responsive">
{% if ipaddress_table.rows %}
{% render_table ipaddress_table 'inc/table.html' %}
{% else %}
@ -81,7 +81,7 @@
</div>
<div class="card">
<h5 class="card-header">Members</h5>
<div class="card-body">
<div class="card-body table-responsive">
{% if members_table.rows %}
{% render_table members_table 'inc/table.html' %}
{% else %}

View File

@ -1,12 +1,15 @@
{% load helpers %}
{% if show_available is not None %}
{% if show_assigned or show_available is not None %}
<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 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>
</div>
{% endif %}

View File

@ -87,7 +87,7 @@
<th scope="row">NAT (inside)</th>
<td>
{% 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 %}
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
{% endif %}
@ -100,7 +100,7 @@
<th scope="row">NAT (outside)</th>
<td>
{% 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 %}
<span class="text-muted">None</span>
{% endif %}
@ -133,8 +133,8 @@
</div>
{% endif %}
</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>
{% endif %}

View File

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

View File

@ -1,4 +1,5 @@
{% extends 'ipam/iprange/base.html' %}
{% load helpers %}
{% block extra_controls %}
{% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
@ -9,9 +10,30 @@
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-12">
{% include 'utilities/obj_table.html' with 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>
<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>
</form>
{% table_config_form table %}
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% 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 %}
<div class="row mb-3">
@ -49,21 +58,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=aggregates_table.paginator page=aggregates_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% 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 %}
<div class="row mb-3">
@ -43,21 +52,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=prefixes_table.paginator page=prefixes_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,9 +1,17 @@
{% extends 'ipam/vlan/base.html' %}
{% load helpers %}
{% block content %}
<div class="row">
<div class="col col-md-12">
{% include 'utilities/obj_table.html' with 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>
</form>
{% table_config_form table %}
{% endblock %}

View File

@ -1,9 +1,17 @@
{% extends 'ipam/vlan/base.html' %}
{% load helpers %}
{% block content %}
<div class="row">
<div class="col col-md-12">
{% include 'utilities/obj_table.html' with 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>
</form>
{% table_config_form table %}
{% endblock %}

View File

@ -11,13 +11,12 @@
{% endif %}
{% endblock %}
{% block buttons %}
{% block extra_controls %}
{% 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
</a>
{% endif %}
{{ block.super }}
{% endblock %}
{% block content %}
@ -66,22 +65,12 @@
<div class="row mb-3">
<div class="col col-md-12">
<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' %}
{% include 'inc/paginator.html' with paginator=vlans_table.paginator page=vlans_table.page %}
</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>
{% endblock %}

View File

@ -75,19 +75,15 @@
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<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>
{% include 'inc/paginator.html' with paginator=assignments_table.paginator page=assignments_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -54,8 +55,8 @@
<h5 class="card-header">
Child Groups
</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>
{% if perms.tenancy.add_contactgroup %}
<div class="card-footer text-end noprint">
@ -71,22 +72,13 @@
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<h5 class="card-header">
Contacts
</h5>
<div class="card-body">
{% include 'inc/table.html' with table=contacts_table %}
<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>
{% 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 %}
</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>
{% endblock %}

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
<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="card">
<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>
{% include 'inc/paginator.html' with paginator=contacts_table.paginator page=contacts_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -9,6 +10,14 @@
{% endfor %}
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -56,22 +65,13 @@
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-header">
Tenants
<h5 class="card-header">Tenants</h5>
<div class="card-body table-responsive">
{% render_table tenants_table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=tenants_table.paginator page=tenants_table.page %}
</div>
<div class="card-body">
{% include 'inc/table.html' with table=tenants_table %}
</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>
{% endblock %}

View File

@ -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 %}

View File

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

View File

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

View File

@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% 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 %}
<div class="row mb-3">
@ -40,21 +49,12 @@
<div class="row">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -1,6 +1,15 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% 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 %}
<div class="row mb-3">
@ -39,21 +48,12 @@
<div class="row">
<div class="col col-md-12">
<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>
{% 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>
{% include 'inc/paginator.html' with paginator=clusters_table.paginator page=clusters_table.page %}
{% plugin_full_width_page object %}
</div>
</div>

View File

@ -59,31 +59,31 @@
<tr>
<th scope="row">Primary IPv4</th>
<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 %}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">Primary IPv6</th>
<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 %}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
</tr>
</table>

View File

@ -1,13 +1,18 @@
{% extends 'virtualization/virtualmachine/base.html' %}
{% load render_table from django_tables2 %}
{% load helpers %}
{% load static %}
{% block content %}
<form method="post">
{% csrf_token %}
{% include 'inc/table_controls.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
{% 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">
{% 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">
@ -32,5 +37,5 @@
<div class="clearfix"></div>
</div>
</form>
{% table_config_form interface_table %}
{% table_config_form table %}
{% endblock %}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block breadcrumbs %}
{{ block.super }}
@ -9,6 +10,14 @@
{% endfor %}
{% 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 %}
<div class="row mb-3">
<div class="col col-md-6">
@ -55,19 +64,12 @@
<div class="col col-md-12">
<div class="card">
<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>
{% 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>
{% endblock %}

5
netbox/utilities/htmx.py Normal file
View File

@ -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

View File

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

View File

@ -4,6 +4,7 @@ from django.db.models import Prefetch
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from dcim.filtersets import DeviceFilterSet
from dcim.models import Device
from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
@ -161,38 +162,34 @@ class ClusterView(generic.ObjectView):
queryset = Cluster.objects.all()
class ClusterVirtualMachinesView(generic.ObjectView):
class ClusterVirtualMachinesView(generic.ObjectChildrenView):
queryset = Cluster.objects.all()
child_model = VirtualMachine
table = tables.VirtualMachineTable
filterset = filtersets.VirtualMachineFilterSet
template_name = 'virtualization/cluster/virtual_machines.html'
def get_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 {
'virtualmachines_table': virtualmachines_table,
'active_tab': 'virtual-machines',
}
class ClusterDevicesView(generic.ObjectView):
class ClusterDevicesView(generic.ObjectChildrenView):
queryset = Cluster.objects.all()
child_model = Device
table = DeviceTable
filterset = DeviceFilterSet
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 {
'devices_table': devices_table,
'active_tab': 'devices',
}
@ -347,26 +344,21 @@ class VirtualMachineView(generic.ObjectView):
}
class VirtualMachineInterfacesView(generic.ObjectView):
class VirtualMachineInterfacesView(generic.ObjectChildrenView):
queryset = VirtualMachine.objects.all()
child_model = VMInterface
table = tables.VMInterfaceTable
filterset = filtersets.VMInterfaceFilterSet
template_name = 'virtualization/virtualmachine/interfaces.html'
def get_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)),
'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 {
'interface_table': interface_table,
'active_tab': 'interfaces',
}