Merge pull request #8073 from netbox-community/8057-htmx-tables
Closes #8057: Dynamic object tables using HTMX
This commit is contained in:
commit
57d3bfcfc9
|
|
@ -797,41 +797,49 @@ class DeviceTypeView(generic.ObjectView):
|
|||
class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
|
||||
child_model = ConsolePortTemplate
|
||||
table = tables.ConsolePortTemplateTable
|
||||
filterset = filtersets.ConsolePortTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
|
||||
child_model = ConsoleServerPortTemplate
|
||||
table = tables.ConsoleServerPortTemplateTable
|
||||
filterset = filtersets.ConsoleServerPortTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypePowerPortsView(DeviceTypeComponentsView):
|
||||
child_model = PowerPortTemplate
|
||||
table = tables.PowerPortTemplateTable
|
||||
filterset = filtersets.PowerPortTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
|
||||
child_model = PowerOutletTemplate
|
||||
table = tables.PowerOutletTemplateTable
|
||||
filterset = filtersets.PowerOutletTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypeInterfacesView(DeviceTypeComponentsView):
|
||||
child_model = InterfaceTemplate
|
||||
table = tables.InterfaceTemplateTable
|
||||
filterset = filtersets.InterfaceTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
|
||||
child_model = FrontPortTemplate
|
||||
table = tables.FrontPortTemplateTable
|
||||
filterset = filtersets.FrontPortTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypeRearPortsView(DeviceTypeComponentsView):
|
||||
child_model = RearPortTemplate
|
||||
table = tables.RearPortTemplateTable
|
||||
filterset = filtersets.RearPortTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
|
||||
child_model = DeviceBayTemplate
|
||||
table = tables.DeviceBayTemplateTable
|
||||
filterset = filtersets.DeviceBayTemplateFilterSet
|
||||
|
||||
|
||||
class DeviceTypeEditView(generic.ObjectEditView):
|
||||
|
|
@ -1328,30 +1336,35 @@ class DeviceView(generic.ObjectView):
|
|||
class DeviceConsolePortsView(DeviceComponentsView):
|
||||
child_model = ConsolePort
|
||||
table = tables.DeviceConsolePortTable
|
||||
filterset = filtersets.ConsolePortFilterSet
|
||||
template_name = 'dcim/device/consoleports.html'
|
||||
|
||||
|
||||
class DeviceConsoleServerPortsView(DeviceComponentsView):
|
||||
child_model = ConsoleServerPort
|
||||
table = tables.DeviceConsoleServerPortTable
|
||||
filterset = filtersets.ConsoleServerPortFilterSet
|
||||
template_name = 'dcim/device/consoleserverports.html'
|
||||
|
||||
|
||||
class DevicePowerPortsView(DeviceComponentsView):
|
||||
child_model = PowerPort
|
||||
table = tables.DevicePowerPortTable
|
||||
filterset = filtersets.PowerPortFilterSet
|
||||
template_name = 'dcim/device/powerports.html'
|
||||
|
||||
|
||||
class DevicePowerOutletsView(DeviceComponentsView):
|
||||
child_model = PowerOutlet
|
||||
table = tables.DevicePowerOutletTable
|
||||
filterset = filtersets.PowerOutletFilterSet
|
||||
template_name = 'dcim/device/poweroutlets.html'
|
||||
|
||||
|
||||
class DeviceInterfacesView(DeviceComponentsView):
|
||||
child_model = Interface
|
||||
table = tables.DeviceInterfaceTable
|
||||
filterset = filtersets.InterfaceFilterSet
|
||||
template_name = 'dcim/device/interfaces.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
|
|
@ -1364,24 +1377,28 @@ class DeviceInterfacesView(DeviceComponentsView):
|
|||
class DeviceFrontPortsView(DeviceComponentsView):
|
||||
child_model = FrontPort
|
||||
table = tables.DeviceFrontPortTable
|
||||
filterset = filtersets.FrontPortFilterSet
|
||||
template_name = 'dcim/device/frontports.html'
|
||||
|
||||
|
||||
class DeviceRearPortsView(DeviceComponentsView):
|
||||
child_model = RearPort
|
||||
table = tables.DeviceRearPortTable
|
||||
filterset = filtersets.RearPortFilterSet
|
||||
template_name = 'dcim/device/rearports.html'
|
||||
|
||||
|
||||
class DeviceDeviceBaysView(DeviceComponentsView):
|
||||
child_model = DeviceBay
|
||||
table = tables.DeviceDeviceBayTable
|
||||
filterset = filtersets.DeviceBayFilterSet
|
||||
template_name = 'dcim/device/devicebays.html'
|
||||
|
||||
|
||||
class DeviceInventoryView(DeviceComponentsView):
|
||||
child_model = InventoryItem
|
||||
table = tables.DeviceInventoryItemTable
|
||||
filterset = filtersets.InventoryItemFilterSet
|
||||
template_name = 'dcim/device/inventory.html'
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
|
|
|
|||
|
|
@ -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_requested_prefixes, add_available_vlans
|
||||
from .utils import add_requested_prefixes, add_available_vlans
|
||||
|
||||
|
||||
#
|
||||
|
|
@ -274,39 +275,32 @@ class AggregateListView(generic.ObjectListView):
|
|||
class AggregateView(generic.ObjectView):
|
||||
queryset = Aggregate.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# Find all child prefixes contained in this aggregate
|
||||
prefix_list = Prefix.objects.restrict(request.user, 'view').filter(
|
||||
prefix__net_contained_or_equal=str(instance.prefix)
|
||||
).prefetch_related(
|
||||
'site', 'role'
|
||||
).order_by(
|
||||
'prefix'
|
||||
)
|
||||
|
||||
# Return List of requested Prefixes
|
||||
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')
|
||||
child_prefixes = add_requested_prefixes(instance.prefix, prefix_list, show_available, show_assigned)
|
||||
|
||||
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 add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'prefix_table': prefix_table,
|
||||
'permissions': permissions,
|
||||
'bulk_querystring': f'within={instance.prefix}',
|
||||
'show_available': show_available,
|
||||
'show_assigned': show_assigned,
|
||||
'active_tab': 'prefixes',
|
||||
'show_available': bool(request.GET.get('show_available', 'true') == 'true'),
|
||||
'show_assigned': bool(request.GET.get('show_assigned', 'true') == 'true'),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -457,17 +451,18 @@ 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):
|
||||
child_prefixes = parent.get_child_prefixes().restrict(request.user, 'view')
|
||||
return parent.get_child_prefixes().restrict(request.user, 'view')
|
||||
|
||||
# Add available prefixes if requested
|
||||
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')
|
||||
child_prefixes = add_requested_prefixes(parent.prefix, child_prefixes, show_available, show_assigned)
|
||||
|
||||
return child_prefixes
|
||||
return add_requested_prefixes(parent.prefix, queryset, show_available, show_assigned)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
|
|
@ -483,6 +478,7 @@ 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):
|
||||
|
|
@ -499,6 +495,7 @@ 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):
|
||||
|
|
@ -560,6 +557,7 @@ 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):
|
||||
|
|
@ -959,6 +957,7 @@ class VLANInterfacesView(generic.ObjectChildrenView):
|
|||
queryset = VLAN.objects.all()
|
||||
child_model = Interface
|
||||
table = tables.VLANDevicesTable
|
||||
filterset = InterfaceFilterSet
|
||||
template_name = 'ipam/vlan/interfaces.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
|
|
@ -974,6 +973,7 @@ class VLANVMInterfacesView(generic.ObjectChildrenView):
|
|||
queryset = VLAN.objects.all()
|
||||
child_model = VMInterface
|
||||
table = tables.VLANVirtualMachinesTable
|
||||
filterset = VMInterfaceFilterSet
|
||||
template_name = 'ipam/vlan/vminterfaces.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -83,17 +84,28 @@ class ObjectChildrenView(ObjectView):
|
|||
queryset = None
|
||||
child_model = None
|
||||
table = None
|
||||
filterset = None
|
||||
template_name = None
|
||||
|
||||
def get_children(self, request, parent):
|
||||
"""
|
||||
Return a QuerySet or iterable of child objects.
|
||||
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.
|
||||
|
|
@ -101,17 +113,27 @@ class ObjectChildrenView(ObjectView):
|
|||
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(child_objects, user=request.user)
|
||||
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,
|
||||
|
|
@ -233,6 +255,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,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { getElements } from '../util';
|
||||
|
||||
function handlePerPageSelect(event: Event): void {
|
||||
const select = event.currentTarget as HTMLSelectElement;
|
||||
if (select.form !== null) {
|
||||
select.form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
export function initPerPage(): void {
|
||||
for (const element of getElements<HTMLSelectElement>('select.per-page')) {
|
||||
element.addEventListener('change', handlePerPageSelect);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import '@popperjs/core';
|
||||
import 'bootstrap';
|
||||
import 'htmx.org';
|
||||
import 'simplebar';
|
||||
import './netbox';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 #}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
@ -38,6 +42,5 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
{% table_config_form table %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -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,9 +42,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DevicePowerPortTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% include 'inc/table_controls.html' with table_modal="DeviceRearPortTable_config" %}
|
||||
<div class="table-responsive">
|
||||
{% 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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 #}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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>…</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>…</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 #}
|
||||
<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>
|
||||
{% if page %}
|
||||
<small class="text-end text-muted">
|
||||
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>…</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 #}
|
||||
<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>
|
||||
{% if page %}
|
||||
<small class="text-end text-muted">
|
||||
Showing {{ page.start_index }}-{{ page.end_index }} of {{ page.paginator.count }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">— {{ table.empty_text }} —</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>
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 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 %}
|
||||
|
|
|
|||
|
|
@ -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 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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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 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 %}
|
||||
|
|
|
|||
|
|
@ -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 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 %}
|
||||
|
|
|
|||
|
|
@ -1,15 +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" %}
|
||||
<div class="table-responsive">
|
||||
{% render_table 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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -165,6 +166,7 @@ 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_children(self, request, parent):
|
||||
|
|
@ -180,6 +182,7 @@ class ClusterDevicesView(generic.ObjectChildrenView):
|
|||
queryset = Cluster.objects.all()
|
||||
child_model = Device
|
||||
table = DeviceTable
|
||||
filterset = DeviceFilterSet
|
||||
template_name = 'virtualization/cluster/devices.html'
|
||||
|
||||
def get_children(self, request, parent):
|
||||
|
|
@ -345,6 +348,7 @@ 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_children(self, request, parent):
|
||||
|
|
|
|||
Loading…
Reference in New Issue