mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-13 16:47:34 -06:00
* Enable HTMX boosting * Refactor HTMX properties for tables * Fix dashboard object list widget * Disable scrolling to page content * Fix initialization of TomSelect dropdowns after HTMX loading * Replace formaction properties with hx-post * Fix quick search field on object list view * Reinitialize copy-to-clipboard buttons upon HTMX load * Disable scrolling effect for intra-page navigation * Introduce user preference for toggling HTMX navigation * Enable HTMX navigation only when selected by user * Pass htmx_navigation context * Fix display of confirmation form when deleting an object * Disable HTMX boosting for rack elevation SVG downloads * Fix dyanmic form rendering * Introduce htmx_boost template tag; enable HTMX for user menu * Use out-of-band sap to update footer stamp * Fix display of toasts after form submission * Fix user preference selection * Misc cleanup * Rename render_partial() to htmx_partial() * Add docstring to htmx_boost template tag * Disable HTMX for user preferences form to force a full page refresh on changes
This commit is contained in:
parent
04d8db7c52
commit
744be59a4d
@ -25,6 +25,7 @@ from netbox.views import generic
|
||||
from netbox.views.generic.base import BaseObjectView
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
@ -320,7 +321,7 @@ class BackgroundTaskListView(TableMixin, BaseRQView):
|
||||
table = self.get_table(data, request, False)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
})
|
||||
@ -489,8 +490,8 @@ class WorkerListView(TableMixin, BaseRQView):
|
||||
table = self.get_table(data, request, False)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if request.htmx.target != 'object_list':
|
||||
if htmx_partial(request):
|
||||
if not request.htmx.target:
|
||||
table.embedded = True
|
||||
# Hide selection checkboxes
|
||||
if 'pk' in table.base_columns:
|
||||
|
@ -20,6 +20,7 @@ from netbox.views import generic
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
from utilities.query import count_related
|
||||
from utilities.querydict import normalize_querydict
|
||||
@ -1224,7 +1225,7 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
||||
}
|
||||
|
||||
# If this is an HTMX request, return only the result HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
response = render(request, 'extras/htmx/script_result.html', context)
|
||||
if job.completed or not job.started:
|
||||
response.status_code = 286
|
||||
|
@ -8,9 +8,11 @@ def settings_and_registry(request):
|
||||
"""
|
||||
Expose Django settings and NetBox registry stores in the template context. Example: {{ settings.DEBUG }}
|
||||
"""
|
||||
user_preferences = request.user.config if request.user.is_authenticated else {}
|
||||
return {
|
||||
'settings': django_settings,
|
||||
'config': get_config(),
|
||||
'registry': registry,
|
||||
'preferences': request.user.config if request.user.is_authenticated else {},
|
||||
'preferences': user_preferences,
|
||||
'htmx_navigation': user_preferences.get('ui.htmx_navigation', False) == 'true'
|
||||
}
|
||||
|
@ -23,6 +23,14 @@ PREFERENCES = {
|
||||
),
|
||||
default='light',
|
||||
),
|
||||
'ui.htmx_navigation': UserPreference(
|
||||
label=_('HTMX Navigation'),
|
||||
choices=(
|
||||
('', _('Disabled')),
|
||||
('true', _('Enabled')),
|
||||
),
|
||||
default=False
|
||||
),
|
||||
'locale.language': UserPreference(
|
||||
label=_('Language'),
|
||||
choices=(
|
||||
|
@ -23,6 +23,7 @@ from utilities.error_handlers import handle_protectederror
|
||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||
from utilities.forms.bulk_import import BulkImportForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.views import GetReturnURLMixin, get_viewname
|
||||
from .base import BaseMultiObjectView
|
||||
@ -161,8 +162,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if request.htmx.target != 'object_list':
|
||||
if htmx_partial(request):
|
||||
if not request.htmx.target:
|
||||
table.embedded = True
|
||||
# Hide selection checkboxes
|
||||
if 'pk' in table.base_columns:
|
||||
|
@ -17,6 +17,7 @@ from extras.signals import clear_events
|
||||
from utilities.error_handlers import handle_protectederror
|
||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.permissions import get_permission_for_model
|
||||
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
||||
from utilities.views import GetReturnURLMixin, get_viewname
|
||||
@ -138,7 +139,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
||||
table = self.get_table(table_data, request, has_bulk_actions)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/table.html', {
|
||||
'object': instance,
|
||||
'table': table,
|
||||
@ -226,7 +227,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
||||
restrict_form_fields(form, request.user)
|
||||
|
||||
# If this is an HTMX request, return only the rendered form HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/form.html', {
|
||||
'form': form,
|
||||
})
|
||||
@ -482,7 +483,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
||||
instance = self.alter_object(self.queryset.model(), request)
|
||||
|
||||
# If this is an HTMX request, return only the rendered form HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/form.html', {
|
||||
'form': form,
|
||||
})
|
||||
|
@ -17,6 +17,7 @@ from netbox.forms import SearchForm
|
||||
from netbox.search import LookupTypes
|
||||
from netbox.search.backends import search_backend
|
||||
from netbox.tables import SearchTable
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
|
||||
__all__ = (
|
||||
@ -104,7 +105,7 @@ class SearchView(View):
|
||||
}).configure(table)
|
||||
|
||||
# If this is an HTMX request, return only the rendered table HTML
|
||||
if request.htmx:
|
||||
if htmx_partial(request):
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
})
|
||||
|
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,11 +1,12 @@
|
||||
import { getElements, isTruthy } from './util';
|
||||
import { initButtons } from './buttons';
|
||||
import { initClipboard } from './clipboard'
|
||||
import { initSelects } from './select';
|
||||
import { initObjectSelector } from './objectSelector';
|
||||
import { initBootstrap } from './bs';
|
||||
import { initMessages } from './messages';
|
||||
|
||||
function initDepedencies(): void {
|
||||
for (const init of [initButtons, initSelects, initObjectSelector, initBootstrap]) {
|
||||
for (const init of [initButtons, initClipboard, initSelects, initObjectSelector, initBootstrap, initMessages]) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
@ -15,16 +16,5 @@ function initDepedencies(): void {
|
||||
* elements.
|
||||
*/
|
||||
export function initHtmx(): void {
|
||||
for (const element of getElements('[hx-target]')) {
|
||||
const targetSelector = element.getAttribute('hx-target');
|
||||
if (isTruthy(targetSelector)) {
|
||||
for (const target of getElements(targetSelector)) {
|
||||
target.addEventListener('htmx:afterSettle', initDepedencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of getElements('[hx-trigger=load]')) {
|
||||
element.addEventListener('htmx:afterSettle', initDepedencies);
|
||||
}
|
||||
document.addEventListener('htmx:afterSettle', initDepedencies);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
@import '../node_modules/@tabler/core/src/scss/vendor/tom-select';
|
||||
|
||||
// Overrides of external libraries
|
||||
@import 'overrides/bootstrap';
|
||||
@import 'overrides/tabler';
|
||||
|
||||
// Transitional styling to ease migration of templates from NetBox v3.x
|
||||
|
4
netbox/project-static/styles/overrides/_bootstrap.scss
Normal file
4
netbox/project-static/styles/overrides/_bootstrap.scss
Normal file
@ -0,0 +1,4 @@
|
||||
// Disable smooth scrolling for intra-page links
|
||||
html {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
{% block title %}{% trans "User Preferences" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="" id="preferences-update">
|
||||
<form method="post" action="" hx-disable="true" id="preferences-update">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Built-in preferences #}
|
||||
|
@ -15,6 +15,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, viewport-fit=cover" />
|
||||
<meta name="htmx-config" content='{"scrollBehavior": "auto"}'>
|
||||
|
||||
{# Page title #}
|
||||
<title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>
|
||||
|
@ -58,8 +58,48 @@ Blocks:
|
||||
<i class="mdi mdi-lightbulb-on"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# User menu #}
|
||||
{% include 'inc/user_menu.html' %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<div class="d-xl-block ps-2">
|
||||
<div>{{ request.user }}</div>
|
||||
<div class="mt-1 small text-secondary">{% if request.user.is_staff %}Staff{% else %}User{% endif %}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow" {% htmx_boost %}>
|
||||
{% if config.DJANGO_ADMIN_ENABLED and request.user.is_staff %}
|
||||
<a class="dropdown-item" href="{% url 'admin:index' %}">
|
||||
<i class="mdi mdi-cog"></i> {% trans "Django Admin" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'account:profile' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-account"></i> {% trans "Profile" %}
|
||||
</a>
|
||||
<a href="{% url 'account:bookmarks' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
|
||||
</a>
|
||||
<a href="{% url 'account:preferences' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-wrench"></i> {% trans "Preferences" %}
|
||||
</a>
|
||||
<a href="{% url 'account:usertoken_list' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-key"></i> {% trans "API Tokens" %}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="{% url 'logout' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-logout-variant"></i> {% trans "Log Out" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="btn-group ps-2">
|
||||
<a class="btn btn-primary" type="button" href="{% url 'login' %}?next={{ request.path }}">
|
||||
<i class="mdi mdi-login-variant"></i> {% trans "Log In" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# /User menu #}
|
||||
</div>
|
||||
|
||||
{# Search box #}
|
||||
@ -79,6 +119,7 @@ Blocks:
|
||||
|
||||
{# Page content #}
|
||||
<div class="page-wrapper">
|
||||
<div id="page-content" {% htmx_boost %}>
|
||||
|
||||
{# Page header #}
|
||||
{% block header %}
|
||||
@ -122,6 +163,8 @@ Blocks:
|
||||
{% endif %}
|
||||
{# /Bottom banner #}
|
||||
|
||||
</div>
|
||||
|
||||
{# Page footer #}
|
||||
<footer class="footer footer-transparent d-print-none py-2">
|
||||
<div class="container-fluid d-flex justify-content-between align-items-center">
|
||||
@ -173,7 +216,7 @@ Blocks:
|
||||
{# /Footer links #}
|
||||
|
||||
{# Footer text #}
|
||||
<ul class="list-inline list-inline-dots mb-0">
|
||||
<ul class="list-inline list-inline-dots mb-0" id="footer-stamp" hx-swap-oob="true">
|
||||
<li class="list-inline-item">
|
||||
{% annotated_now %} {% now 'T' %}
|
||||
</li>
|
||||
|
@ -10,7 +10,7 @@
|
||||
{% endif %}
|
||||
{% if 'bulk_rename' in actions %}
|
||||
{% with bulk_rename_view=model|validated_viewname:"bulk_rename" %}
|
||||
<button type="submit" name="_rename" formaction="{% url bulk_rename_view %}" class="btn btn-outline-warning">
|
||||
<button type="submit" name="_rename" {% formaction %}="{% url bulk_rename_view %}" class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename Selected" %}
|
||||
</button>
|
||||
{% endwith %}
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||
class="btn btn-warning">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||
</button>
|
||||
@ -14,7 +14,7 @@
|
||||
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
||||
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -11,7 +11,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||
<button type="submit" name="_disconnect"
|
||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||
</button>
|
||||
|
@ -11,63 +11,63 @@
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.dcim.add_consoleport %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Console Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_consoleserverport %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
||||
{% trans "Console Server Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_powerport %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Power Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_poweroutlet %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Power Outlets" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_interface %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
|
||||
class="dropdown-item">{% trans "Interfaces" %}
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Interfaces" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_rearport %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Rear Ports" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_devicebay %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Device Bays" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_modulebay %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Module Bays" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_inventoryitem %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Inventory Items" %}
|
||||
</button>
|
||||
</li>
|
||||
@ -78,7 +78,7 @@
|
||||
{% if 'bulk_edit' in actions %}
|
||||
<div class="btn-group" role="group">
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
<button type="submit" name="_rename" formaction="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning">
|
||||
<button type="submit" name="_rename" {% formaction %}="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||
class="btn btn-warning">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||
</button>
|
||||
@ -16,7 +16,7 @@
|
||||
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
||||
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||
</button>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" class="rack_elevation"></object>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}">
|
||||
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" hx-boost="false">
|
||||
<i class="mdi mdi-file-download"></i> {% trans "Download SVG" %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -13,13 +13,13 @@
|
||||
</div>
|
||||
<div class="card-footer d-print-none">
|
||||
{% if table.rows %}
|
||||
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-warning">
|
||||
<button type="submit" name="_edit" {% formaction %}="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-warning">
|
||||
<span class="mdi mdi-pencil-outline" aria-hidden="true"></span> {% trans "Rename" %}
|
||||
</button>
|
||||
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-warning">
|
||||
<button type="submit" name="_edit" {% formaction %}="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-warning">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
||||
</button>
|
||||
<button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-danger">
|
||||
<button type="submit" name="_delete" {% formaction %}="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-danger">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -52,17 +52,17 @@
|
||||
{% htmx_table 'dcim:powerfeed_list' power_panel_id=object.pk %}
|
||||
<div class="card-footer d-print-none">
|
||||
{% 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">
|
||||
<button type="submit" name="_edit" {% formaction %}="{% url 'dcim:powerfeed_bulk_edit' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-warning">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "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">
|
||||
<button type="submit" name="_disconnect" {% formaction %}="{% url 'dcim:powerfeed_bulk_disconnect' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-outline-danger">
|
||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "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">
|
||||
<button type="submit" name="_delete" {% formaction %}="{% url 'dcim:powerfeed_bulk_delete' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-danger">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.extras.sync_configcontext %}
|
||||
<button type="submit" name="_sync" formaction="{% url 'extras:configcontext_bulk_sync' %}" class="btn btn-primary">
|
||||
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configcontext_bulk_sync' %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.extras.sync_configtemplate %}
|
||||
<button type="submit" name="_sync" formaction="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary">
|
||||
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% if htmx_url and has_permission %}
|
||||
<div class="htmx-container" hx-get="{{ htmx_url }}" hx-trigger="load"></div>
|
||||
<div class="htmx-container" hx-get="{{ htmx_url }}" hx-trigger="load" hx-target="this" hx-select="table" hx-swap="innerHTML"></div>
|
||||
{% elif htmx_url %}
|
||||
<div class="text-muted text-center">
|
||||
<i class="mdi mdi-lock-outline"></i> {% trans "No permission to view this content" %}.
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block bulk_buttons %}
|
||||
{% if perms.extras.sync_configcontext %}
|
||||
<button type="submit" name="_sync" formaction="{% url 'extras:exporttemplate_bulk_sync' %}" class="btn btn-primary">
|
||||
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:exporttemplate_bulk_sync' %}" class="btn btn-primary">
|
||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -21,7 +21,7 @@
|
||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||
<button type="submit" name="_edit"
|
||||
formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_edit_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-warning">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
||||
</button>
|
||||
@ -35,7 +35,7 @@
|
||||
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
|
||||
{% if 'bulk_delete' in actions and bulk_delete_view %}
|
||||
<button type="submit"
|
||||
formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
||||
class="btn btn-danger">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
||||
</button>
|
||||
|
@ -56,7 +56,7 @@ Context:
|
||||
<form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
|
||||
{% csrf_token %}
|
||||
|
||||
<div id="form_fields">
|
||||
<div id="form_fields" hx-disinherit="hx-select hx-swap">
|
||||
{% block form %}
|
||||
{% include 'htmx/form.html' %}
|
||||
{% endblock form %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div id="django-messages" class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div id="django-messages" class="toast-container position-fixed bottom-0 end-0 p-3" hx-swap-oob="true">
|
||||
|
||||
{# Non-Field Form Errors #}
|
||||
{% if form and form.non_field_errors %}
|
||||
|
@ -2,7 +2,12 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if page %}
|
||||
<div class="d-flex justify-content-between align-items-center border-top p-2">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center border-top p-2"
|
||||
hx-target="closest .htmx-container"
|
||||
hx-disinherit="hx-select hx-swap"
|
||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
||||
>
|
||||
|
||||
{# Pages carousel #}
|
||||
{% if paginator.num_pages > 1 %}
|
||||
@ -13,12 +18,7 @@
|
||||
{% if page.has_previous %}
|
||||
<li class="page-item">
|
||||
{% if htmx %}
|
||||
<a href="#"
|
||||
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="page-link"
|
||||
>
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}" class="page-link">
|
||||
<i class="mdi mdi-chevron-left"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
@ -34,12 +34,7 @@
|
||||
{% for p in page.smart_pages %}
|
||||
<li class="page-item{% if page.number == p %} active" aria-current="page{% endif %}">
|
||||
{% if p and htmx %}
|
||||
<a href="#"
|
||||
hx-get="{{ table.htmx_url }}{% querystring request page=p %}"
|
||||
hx-target="closest .htmx-container"
|
||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
||||
class="page-link"
|
||||
>
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=p %}" class="page-link">
|
||||
{{ p }}
|
||||
</a>
|
||||
{% elif p %}
|
||||
@ -57,12 +52,7 @@
|
||||
{% if page.has_next %}
|
||||
<li class="page-item">
|
||||
{% if htmx %}
|
||||
<a href="#"
|
||||
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="page-link"
|
||||
>
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}" class="page-link">
|
||||
<i class="mdi mdi-chevron-right"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
@ -97,12 +87,7 @@
|
||||
<div class="dropdown-menu">
|
||||
{% for n in page.paginator.get_page_lengths %}
|
||||
{% if htmx %}
|
||||
<a href="#"
|
||||
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>
|
||||
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
||||
{% else %}
|
||||
<a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
||||
{% endif %}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-auto d-print-none">
|
||||
<div class="input-group input-group-flat me-2 quicksearch">
|
||||
<div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap">
|
||||
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search"
|
||||
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
||||
<span class="input-group-text py-1">
|
||||
|
@ -1,7 +1,11 @@
|
||||
{% load django_tables2 %}
|
||||
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
||||
{% if table.show_header %}
|
||||
<thead>
|
||||
<thead
|
||||
hx-target="closest .htmx-container"
|
||||
hx-disinherit="hx-select hx-swap"
|
||||
{% if not table.embedded %} hx-push-url="true"{% endif %}
|
||||
>
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
{% if column.orderable %}
|
||||
@ -10,16 +14,12 @@
|
||||
<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 %}
|
||||
|
@ -1,41 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<div class="d-xl-block ps-2">
|
||||
<div>{{ request.user }}</div>
|
||||
<div class="mt-1 small text-secondary">{% if request.user.is_staff %}Staff{% else %}User{% endif %}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
{% if config.DJANGO_ADMIN_ENABLED and request.user.is_staff %}
|
||||
<a class="dropdown-item" href="{% url 'admin:index' %}">
|
||||
<i class="mdi mdi-cog"></i> {% trans "Django Admin" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'account:profile' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-account"></i> {% trans "Profile" %}
|
||||
</a>
|
||||
<a href="{% url 'account:bookmarks' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
|
||||
</a>
|
||||
<a href="{% url 'account:preferences' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-wrench"></i> {% trans "Preferences" %}
|
||||
</a>
|
||||
<a href="{% url 'account:usertoken_list' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-key"></i> {% trans "API Tokens" %}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="{% url 'logout' %}" class="dropdown-item">
|
||||
<i class="mdi mdi-logout-variant"></i> {% trans "Log Out" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="btn-group ps-2">
|
||||
<a class="btn btn-primary" type="button" href="{% url 'login' %}?next={{ request.path }}">
|
||||
<i class="mdi mdi-login-variant"></i> {% trans "Log In" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
@ -5,7 +5,7 @@
|
||||
{{ block.super }}
|
||||
{% if 'bulk_remove_devices' in actions %}
|
||||
<button type="submit" name="_remove"
|
||||
formaction="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
|
||||
class="btn btn-danger">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
|
||||
</button>
|
||||
|
@ -6,7 +6,7 @@
|
||||
{{ block.super }}
|
||||
{% if 'bulk_rename' in actions %}
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
|
@ -6,7 +6,7 @@
|
||||
{{ block.super }}
|
||||
{% if 'bulk_rename' in actions %}
|
||||
<button type="submit" name="_rename"
|
||||
formaction="{% url 'virtualization:virtualdisk_bulk_rename' %}?return_url={{ return_url }}"
|
||||
{% formaction %}="{% url 'virtualization:virtualdisk_bulk_rename' %}?return_url={{ return_url }}"
|
||||
class="btn btn-outline-warning">
|
||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||
</button>
|
||||
|
@ -10,14 +10,14 @@
|
||||
<ul class="dropdown-menu">
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Interfaces" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<li>
|
||||
<button type="submit" formaction="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||
{% trans "Virtual Disks" %}
|
||||
</button>
|
||||
</li>
|
||||
|
@ -55,7 +55,8 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
||||
class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'locale.language', 'pagination.per_page', 'pagination.placement', 'ui.colormode', name=_('User Interface')
|
||||
'locale.language', 'pagination.per_page', 'pagination.placement', 'ui.colormode', 'ui.htmx_navigation',
|
||||
name=_('User Interface')
|
||||
),
|
||||
FieldSet('data_format', name=_('Miscellaneous')),
|
||||
)
|
||||
|
13
netbox/utilities/htmx.py
Normal file
13
netbox/utilities/htmx.py
Normal file
@ -0,0 +1,13 @@
|
||||
__all__ = (
|
||||
'htmx_partial',
|
||||
)
|
||||
|
||||
PAGE_CONTAINER_ID = 'page-content'
|
||||
|
||||
|
||||
def htmx_partial(request):
|
||||
"""
|
||||
Determines whether to render partial (versus complete) HTML content
|
||||
in response to an HTMX request, based on the target element.
|
||||
"""
|
||||
return request.htmx and request.htmx.target != PAGE_CONTAINER_ID
|
@ -1,4 +1,5 @@
|
||||
<div class="card-body htmx-container table-responsive p-0"
|
||||
hx-get="{% url viewname %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}"
|
||||
hx-trigger="load"
|
||||
hx-target="this"
|
||||
hx-trigger="load" hx-select="table" hx-swap="innerHTML"
|
||||
></div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% if url %}
|
||||
<button type="submit" name="_delete" formaction="{{ url }}" class="btn btn-red">
|
||||
<button type="submit" name="_delete" {% formaction %}="{{ url }}" class="btn btn-red">
|
||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% if url %}
|
||||
<button type="submit" name="_edit" formaction="{{ url }}" class="btn btn-yellow">
|
||||
<button type="submit" name="_edit" {% formaction %}="{{ url }}" class="btn btn-yellow">
|
||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
@ -2,6 +2,8 @@
|
||||
<a href="#"
|
||||
hx-get="{{ url }}"
|
||||
hx-target="#htmx-modal-content"
|
||||
hx-swap="innerHTML"
|
||||
hx-select="form"
|
||||
class="btn btn-red"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% load helpers %}
|
||||
{% load navigation %}
|
||||
|
||||
<ul class="navbar-nav pt-lg-2">
|
||||
<ul class="navbar-nav pt-lg-2" {% htmx_boost %}>
|
||||
{% for menu, groups in nav_items %}
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
|
@ -8,6 +8,7 @@ __all__ = (
|
||||
'checkmark',
|
||||
'copy_content',
|
||||
'customfield_value',
|
||||
'formaction',
|
||||
'tag',
|
||||
)
|
||||
|
||||
@ -113,3 +114,14 @@ def htmx_table(context, viewname, return_url=None, **kwargs):
|
||||
'viewname': viewname,
|
||||
'url_params': url_params,
|
||||
}
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def formaction(context):
|
||||
"""
|
||||
Replace the 'formaction' attribute on an HTML element with the appropriate HTMX attributes
|
||||
if HTMX navigation is enabled (per the user's preferences).
|
||||
"""
|
||||
if context.get('htmx_navigation', False):
|
||||
return 'hx-push-url="true" hx-post'
|
||||
return 'formaction'
|
||||
|
@ -1,11 +1,11 @@
|
||||
from typing import Dict
|
||||
from django import template
|
||||
from django.template import Context
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from netbox.navigation.menu import MENUS
|
||||
|
||||
__all__ = (
|
||||
'nav',
|
||||
'htmx_boost',
|
||||
)
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("navigation/menu.html", takes_context=True)
|
||||
def nav(context: Context) -> Dict:
|
||||
def nav(context):
|
||||
"""
|
||||
Render the navigation menu.
|
||||
"""
|
||||
@ -40,6 +40,31 @@ def nav(context: Context) -> Dict:
|
||||
nav_items.append((menu, groups))
|
||||
|
||||
return {
|
||||
"nav_items": nav_items,
|
||||
"request": context["request"]
|
||||
'nav_items': nav_items,
|
||||
'htmx_navigation': context['htmx_navigation']
|
||||
}
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def htmx_boost(context, target='#page-content', select='#page-content'):
|
||||
"""
|
||||
Renders the HTML attributes needed to effect HTMX boosting within an element if
|
||||
HTMX navigation is enabled for the request. The target and select parameters are
|
||||
rendered as `hx-target` and `hx-select`, respectively. For example:
|
||||
|
||||
<div id="page-content" {% htmx_boost %}>
|
||||
|
||||
If HTMX navigation is not enabled, the tag renders no content.
|
||||
"""
|
||||
if not context.get('htmx_navigation', False):
|
||||
return ''
|
||||
hx_params = {
|
||||
'hx-boost': 'true',
|
||||
'hx-target': target,
|
||||
'hx-select': select,
|
||||
'hx-swap': 'outerHTML show:window:top',
|
||||
}
|
||||
htmx_params = ' '.join([
|
||||
f'{k}="{v}"' for k, v in hx_params.items()
|
||||
])
|
||||
return mark_safe(htmx_params)
|
||||
|
Loading…
Reference in New Issue
Block a user