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

* Enable HTMX rendering for embedded tables

* Start converting embedded tables to use HTMX (WIP)

* Additional table conversions (WIP)

* Standardize HTMX usage for nested group models

* Enable HTMX for additional emebedded tables

* Fix HTMX table rendering for ObjectChildrenView

* Standardize usage of inc/panel_table.html

* Hide selection boxes in embedded tables
This commit is contained in:
Jeremy Stretch 2023-01-16 15:50:45 -05:00 committed by jeremystretch
parent f74a2536f1
commit 1a2dae3471
65 changed files with 381 additions and 667 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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