This commit is contained in:
Jeremy Stretch 2024-03-27 16:12:03 +00:00 committed by GitHub
commit 38bc314713
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 213 additions and 160 deletions

View File

@ -25,6 +25,7 @@ from netbox.views import generic
from netbox.views.generic.base import BaseObjectView from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.htmx import htmx_partial
from utilities.query import count_related from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
@ -320,7 +321,7 @@ class BackgroundTaskListView(TableMixin, BaseRQView):
table = self.get_table(data, request, False) table = self.get_table(data, request, False)
# If this is an HTMX request, return only the rendered table HTML # 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', { return render(request, 'htmx/table.html', {
'table': table, 'table': table,
}) })
@ -489,8 +490,8 @@ class WorkerListView(TableMixin, BaseRQView):
table = self.get_table(data, request, False) table = self.get_table(data, request, False)
# If this is an HTMX request, return only the rendered table HTML # If this is an HTMX request, return only the rendered table HTML
if request.htmx: if htmx_partial(request):
if request.htmx.target != 'object_list': if not request.htmx.target:
table.embedded = True table.embedded = True
# Hide selection checkboxes # Hide selection checkboxes
if 'pk' in table.base_columns: if 'pk' in table.base_columns:

View File

@ -20,6 +20,7 @@ from netbox.views import generic
from netbox.views.generic.mixins import TableMixin from netbox.views.generic.mixins import TableMixin
from utilities.data import shallow_compare_dict from utilities.data import shallow_compare_dict
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
from utilities.htmx import htmx_partial
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.query import count_related from utilities.query import count_related
from utilities.querydict import normalize_querydict 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 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) response = render(request, 'extras/htmx/script_result.html', context)
if job.completed or not job.started: if job.completed or not job.started:
response.status_code = 286 response.status_code = 286

View File

@ -8,9 +8,11 @@ def settings_and_registry(request):
""" """
Expose Django settings and NetBox registry stores in the template context. Example: {{ settings.DEBUG }} 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 { return {
'settings': django_settings, 'settings': django_settings,
'config': get_config(), 'config': get_config(),
'registry': registry, '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'
} }

View File

@ -23,6 +23,14 @@ PREFERENCES = {
), ),
default='light', default='light',
), ),
'ui.htmx_navigation': UserPreference(
label=_('HTMX Navigation'),
choices=(
('', _('Disabled')),
('true', _('Enabled')),
),
default=False
),
'locale.language': UserPreference( 'locale.language': UserPreference(
label=_('Language'), label=_('Language'),
choices=( choices=(

View File

@ -23,6 +23,7 @@ from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
from utilities.forms.bulk_import import BulkImportForm from utilities.forms.bulk_import import BulkImportForm
from utilities.htmx import htmx_partial
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.views import GetReturnURLMixin, get_viewname from utilities.views import GetReturnURLMixin, get_viewname
from .base import BaseMultiObjectView from .base import BaseMultiObjectView
@ -161,8 +162,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
table = self.get_table(self.queryset, request, has_bulk_actions) table = self.get_table(self.queryset, request, has_bulk_actions)
# If this is an HTMX request, return only the rendered table HTML # If this is an HTMX request, return only the rendered table HTML
if request.htmx: if htmx_partial(request):
if request.htmx.target != 'object_list': if not request.htmx.target:
table.embedded = True table.embedded = True
# Hide selection checkboxes # Hide selection checkboxes
if 'pk' in table.base_columns: if 'pk' in table.base_columns:

View File

@ -17,6 +17,7 @@ from extras.signals import clear_events
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortRequest, PermissionsViolation from utilities.exceptions import AbortRequest, PermissionsViolation
from utilities.forms import ConfirmationForm, restrict_form_fields from utilities.forms import ConfirmationForm, restrict_form_fields
from utilities.htmx import htmx_partial
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.querydict import normalize_querydict, prepare_cloned_fields from utilities.querydict import normalize_querydict, prepare_cloned_fields
from utilities.views import GetReturnURLMixin, get_viewname 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) table = self.get_table(table_data, request, has_bulk_actions)
# If this is an HTMX request, return only the rendered table HTML # 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', { return render(request, 'htmx/table.html', {
'object': instance, 'object': instance,
'table': table, 'table': table,
@ -226,7 +227,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
restrict_form_fields(form, request.user) restrict_form_fields(form, request.user)
# If this is an HTMX request, return only the rendered form HTML # 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', { return render(request, 'htmx/form.html', {
'form': form, 'form': form,
}) })
@ -482,7 +483,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
instance = self.alter_object(self.queryset.model(), request) instance = self.alter_object(self.queryset.model(), request)
# If this is an HTMX request, return only the rendered form HTML # 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', { return render(request, 'htmx/form.html', {
'form': form, 'form': form,
}) })

View File

@ -17,6 +17,7 @@ from netbox.forms import SearchForm
from netbox.search import LookupTypes from netbox.search import LookupTypes
from netbox.search.backends import search_backend from netbox.search.backends import search_backend
from netbox.tables import SearchTable from netbox.tables import SearchTable
from utilities.htmx import htmx_partial
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
__all__ = ( __all__ = (
@ -104,7 +105,7 @@ class SearchView(View):
}).configure(table) }).configure(table)
# If this is an HTMX request, return only the rendered table HTML # 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', { return render(request, 'htmx/table.html', {
'table': table, '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

View File

@ -1,11 +1,12 @@
import { getElements, isTruthy } from './util';
import { initButtons } from './buttons'; import { initButtons } from './buttons';
import { initClipboard } from './clipboard'
import { initSelects } from './select'; import { initSelects } from './select';
import { initObjectSelector } from './objectSelector'; import { initObjectSelector } from './objectSelector';
import { initBootstrap } from './bs'; import { initBootstrap } from './bs';
import { initMessages } from './messages';
function initDepedencies(): void { function initDepedencies(): void {
for (const init of [initButtons, initSelects, initObjectSelector, initBootstrap]) { for (const init of [initButtons, initClipboard, initSelects, initObjectSelector, initBootstrap, initMessages]) {
init(); init();
} }
} }
@ -15,16 +16,5 @@ function initDepedencies(): void {
* elements. * elements.
*/ */
export function initHtmx(): void { export function initHtmx(): void {
for (const element of getElements('[hx-target]')) { document.addEventListener('htmx:afterSettle', initDepedencies);
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);
}
} }

View File

@ -5,6 +5,7 @@
@import '../node_modules/@tabler/core/src/scss/vendor/tom-select'; @import '../node_modules/@tabler/core/src/scss/vendor/tom-select';
// Overrides of external libraries // Overrides of external libraries
@import 'overrides/bootstrap';
@import 'overrides/tabler'; @import 'overrides/tabler';
// Transitional styling to ease migration of templates from NetBox v3.x // Transitional styling to ease migration of templates from NetBox v3.x

View File

@ -0,0 +1,4 @@
// Disable smooth scrolling for intra-page links
html {
scroll-behavior: auto !important;
}

View File

@ -6,7 +6,7 @@
{% block title %}{% trans "User Preferences" %}{% endblock %} {% block title %}{% trans "User Preferences" %}{% endblock %}
{% block content %} {% block content %}
<form method="post" action="" id="preferences-update"> <form method="post" action="" hx-disable="true" id="preferences-update">
{% csrf_token %} {% csrf_token %}
{# Built-in preferences #} {# Built-in preferences #}

View File

@ -15,6 +15,7 @@
<head> <head>
<meta charset="UTF-8" /> <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="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 #} {# Page title #}
<title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title> <title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>

View File

@ -58,8 +58,48 @@ Blocks:
<i class="mdi mdi-lightbulb-on"></i> <i class="mdi mdi-lightbulb-on"></i>
</button> </button>
</div> </div>
{# User menu #} {# 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> </div>
{# Search box #} {# Search box #}
@ -79,6 +119,7 @@ Blocks:
{# Page content #} {# Page content #}
<div class="page-wrapper"> <div class="page-wrapper">
<div id="page-content" {% htmx_boost %}>
{# Page header #} {# Page header #}
{% block header %} {% block header %}
@ -122,6 +163,8 @@ Blocks:
{% endif %} {% endif %}
{# /Bottom banner #} {# /Bottom banner #}
</div>
{# Page footer #} {# Page footer #}
<footer class="footer footer-transparent d-print-none py-2"> <footer class="footer footer-transparent d-print-none py-2">
<div class="container-fluid d-flex justify-content-between align-items-center"> <div class="container-fluid d-flex justify-content-between align-items-center">
@ -173,7 +216,7 @@ Blocks:
{# /Footer links #} {# /Footer links #}
{# Footer text #} {# 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"> <li class="list-inline-item">
{% annotated_now %} {% now 'T' %} {% annotated_now %} {% now 'T' %}
</li> </li>

View File

@ -10,7 +10,7 @@
{% endif %} {% endif %}
{% if 'bulk_rename' in actions %} {% if 'bulk_rename' in actions %}
{% with bulk_rename_view=model|validated_viewname:"bulk_rename" %} {% 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" %} <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename Selected" %}
</button> </button>
{% endwith %} {% endwith %}

View File

@ -5,7 +5,7 @@
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %} {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
{% if 'bulk_edit' in actions and bulk_edit_view %} {% if 'bulk_edit' in actions and bulk_edit_view %}
<button type="submit" name="_edit" <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"> class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button> </button>
@ -14,7 +14,7 @@
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %} {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
{% if 'bulk_rename' in actions and bulk_rename_view %} {% if 'bulk_rename' in actions and bulk_rename_view %}
<button type="submit" name="_rename" <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"> class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button> </button>

View File

@ -7,7 +7,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -7,7 +7,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -7,7 +7,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -11,7 +11,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -7,7 +7,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -7,7 +7,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -7,7 +7,7 @@
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %} {% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %} {% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
<button type="submit" name="_disconnect" <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"> class="btn btn-outline-danger">
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>

View File

@ -11,63 +11,63 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.dcim.add_consoleport %} {% if perms.dcim.add_consoleport %}
<li> <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" %} {% trans "Console Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_consoleserverport %} {% if perms.dcim.add_consoleserverport %}
<li> <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" %} {% trans "Console Server Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_powerport %} {% if perms.dcim.add_powerport %}
<li> <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" %} {% trans "Power Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_poweroutlet %} {% if perms.dcim.add_poweroutlet %}
<li> <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" %} {% trans "Power Outlets" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_interface %} {% if perms.dcim.add_interface %}
<li> <li>
<button type="submit" formaction="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" <button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
class="dropdown-item">{% trans "Interfaces" %} {% trans "Interfaces" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_rearport %} {% if perms.dcim.add_rearport %}
<li> <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" %} {% trans "Rear Ports" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_devicebay %} {% if perms.dcim.add_devicebay %}
<li> <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" %} {% trans "Device Bays" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_modulebay %} {% if perms.dcim.add_modulebay %}
<li> <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" %} {% trans "Module Bays" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.dcim.add_inventoryitem %} {% if perms.dcim.add_inventoryitem %}
<li> <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" %} {% trans "Inventory Items" %}
</button> </button>
</li> </li>
@ -78,7 +78,7 @@
{% if 'bulk_edit' in actions %} {% if 'bulk_edit' in actions %}
<div class="btn-group" role="group"> <div class="btn-group" role="group">
{% bulk_edit_button model query_params=request.GET %} {% 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" %} <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>
</div> </div>

View File

@ -7,7 +7,7 @@
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %} {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
{% if 'bulk_edit' in actions and bulk_edit_view %} {% if 'bulk_edit' in actions and bulk_edit_view %}
<button type="submit" name="_edit" <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"> class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button> </button>
@ -16,7 +16,7 @@
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %} {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
{% if 'bulk_rename' in actions and bulk_rename_view %} {% if 'bulk_rename' in actions and bulk_rename_view %}
<button type="submit" name="_rename" <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"> class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
</button> </button>

View File

@ -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> <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>
<div class="text-center mt-3"> <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" %} <i class="mdi mdi-file-download"></i> {% trans "Download SVG" %}
</a> </a>
</div> </div>

View File

@ -13,13 +13,13 @@
</div> </div>
<div class="card-footer d-print-none"> <div class="card-footer d-print-none">
{% if table.rows %} {% 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" %} <span class="mdi mdi-pencil-outline" aria-hidden="true"></span> {% trans "Rename" %}
</button> </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" %} <span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</button> </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" %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -52,17 +52,17 @@
{% htmx_table 'dcim:powerfeed_list' power_panel_id=object.pk %} {% htmx_table 'dcim:powerfeed_list' power_panel_id=object.pk %}
<div class="card-footer d-print-none"> <div class="card-footer d-print-none">
{% if perms.dcim.change_powerfeed %} {% 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" %} <span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.delete_cable %} {% 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" %} <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
</button> </button>
{% endif %} {% endif %}
{% if perms.dcim.delete_powerfeed %} {% 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" %} <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -3,7 +3,7 @@
{% block bulk_buttons %} {% block bulk_buttons %}
{% if perms.extras.sync_configcontext %} {% 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" %} <i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -3,7 +3,7 @@
{% block bulk_buttons %} {% block bulk_buttons %}
{% if perms.extras.sync_configtemplate %} {% 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" %} <i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
{% if htmx_url and has_permission %} {% 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 %} {% elif htmx_url %}
<div class="text-muted text-center"> <div class="text-muted text-center">
<i class="mdi mdi-lock-outline"></i> {% trans "No permission to view this content" %}. <i class="mdi mdi-lock-outline"></i> {% trans "No permission to view this content" %}.

View File

@ -3,7 +3,7 @@
{% block bulk_buttons %} {% block bulk_buttons %}
{% if perms.extras.sync_configcontext %} {% 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" %} <i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -21,7 +21,7 @@
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %} {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
{% if 'bulk_edit' in actions and bulk_edit_view %} {% if 'bulk_edit' in actions and bulk_edit_view %}
<button type="submit" name="_edit" <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"> class="btn btn-warning">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %} <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
</button> </button>
@ -35,7 +35,7 @@
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %} {% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
{% if 'bulk_delete' in actions and bulk_delete_view %} {% if 'bulk_delete' in actions and bulk_delete_view %}
<button type="submit" <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"> class="btn btn-danger">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
</button> </button>

View File

@ -56,7 +56,7 @@ Context:
<form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5"> <form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
{% csrf_token %} {% csrf_token %}
<div id="form_fields"> <div id="form_fields" hx-disinherit="hx-select hx-swap">
{% block form %} {% block form %}
{% include 'htmx/form.html' %} {% include 'htmx/form.html' %}
{% endblock form %} {% endblock form %}

View File

@ -1,6 +1,6 @@
{% load helpers %} {% 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 #} {# Non-Field Form Errors #}
{% if form and form.non_field_errors %} {% if form and form.non_field_errors %}

View File

@ -2,7 +2,12 @@
{% load i18n %} {% load i18n %}
{% if page %} {% 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 #} {# Pages carousel #}
{% if paginator.num_pages > 1 %} {% if paginator.num_pages > 1 %}
@ -13,12 +18,7 @@
{% if page.has_previous %} {% if page.has_previous %}
<li class="page-item"> <li class="page-item">
{% if htmx %} {% if htmx %}
<a href="#" <a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}" class="page-link">
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"
>
<i class="mdi mdi-chevron-left"></i> <i class="mdi mdi-chevron-left"></i>
</a> </a>
{% else %} {% else %}
@ -34,12 +34,7 @@
{% for p in page.smart_pages %} {% for p in page.smart_pages %}
<li class="page-item{% if page.number == p %} active" aria-current="page{% endif %}"> <li class="page-item{% if page.number == p %} active" aria-current="page{% endif %}">
{% if p and htmx %} {% if p and htmx %}
<a href="#" <a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=p %}" class="page-link">
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"
>
{{ p }} {{ p }}
</a> </a>
{% elif p %} {% elif p %}
@ -57,12 +52,7 @@
{% if page.has_next %} {% if page.has_next %}
<li class="page-item"> <li class="page-item">
{% if htmx %} {% if htmx %}
<a href="#" <a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}" class="page-link">
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"
>
<i class="mdi mdi-chevron-right"></i> <i class="mdi mdi-chevron-right"></i>
</a> </a>
{% else %} {% else %}
@ -97,12 +87,7 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
{% for n in page.paginator.get_page_lengths %} {% for n in page.paginator.get_page_lengths %}
{% if htmx %} {% if htmx %}
<a href="#" <a href="#" hx-get="{{ table.htmx_url }}{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
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>
{% else %} {% else %}
<a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a> <a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
{% endif %} {% endif %}

View File

@ -3,7 +3,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-auto d-print-none"> <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" <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" /> hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
<span class="input-group-text py-1"> <span class="input-group-text py-1">

View File

@ -1,7 +1,11 @@
{% load django_tables2 %} {% load django_tables2 %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}> <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
{% if table.show_header %} {% 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> <tr>
{% for column in table.columns %} {% for column in table.columns %}
{% if column.orderable %} {% if column.orderable %}
@ -10,16 +14,12 @@
<div class="float-end"> <div class="float-end">
<a href="#" <a href="#"
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field='' %}" 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" class="text-danger"
><i class="mdi mdi-close"></i></a> ><i class="mdi mdi-close"></i></a>
</div> </div>
{% endif %} {% endif %}
<a href="#" <a href="#"
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}" 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> >{{ column.header }}</a>
</th> </th>
{% else %} {% else %}

View File

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

View File

@ -5,7 +5,7 @@
{{ block.super }} {{ block.super }}
{% if 'bulk_remove_devices' in actions %} {% if 'bulk_remove_devices' in actions %}
<button type="submit" name="_remove" <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"> class="btn btn-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %} <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
</button> </button>

View File

@ -6,7 +6,7 @@
{{ block.super }} {{ block.super }}
{% if 'bulk_rename' in actions %} {% if 'bulk_rename' in actions %}
<button type="submit" name="_rename" <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"> class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %} <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>

View File

@ -6,7 +6,7 @@
{{ block.super }} {{ block.super }}
{% if 'bulk_rename' in actions %} {% if 'bulk_rename' in actions %}
<button type="submit" name="_rename" <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"> class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %} <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
</button> </button>

View File

@ -10,14 +10,14 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% if perms.virtualization.add_vminterface %} {% if perms.virtualization.add_vminterface %}
<li> <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" %} {% trans "Interfaces" %}
</button> </button>
</li> </li>
{% endif %} {% endif %}
{% if perms.virtualization.add_virtualdisk %} {% if perms.virtualization.add_virtualdisk %}
<li> <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" %} {% trans "Virtual Disks" %}
</button> </button>
</li> </li>

View File

@ -55,7 +55,8 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass): class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
fieldsets = ( fieldsets = (
FieldSet( 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')), FieldSet('data_format', name=_('Miscellaneous')),
) )

13
netbox/utilities/htmx.py Normal file
View 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

View File

@ -1,4 +1,5 @@
<div class="card-body htmx-container table-responsive p-0" <div class="card-body htmx-container table-responsive p-0"
hx-get="{% url viewname %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}" 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> ></div>

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
{% if url %} {% 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" %} <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
{% if url %} {% 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" %} <i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
</button> </button>
{% endif %} {% endif %}

View File

@ -2,6 +2,8 @@
<a href="#" <a href="#"
hx-get="{{ url }}" hx-get="{{ url }}"
hx-target="#htmx-modal-content" hx-target="#htmx-modal-content"
hx-swap="innerHTML"
hx-select="form"
class="btn btn-red" class="btn btn-red"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#htmx-modal" data-bs-target="#htmx-modal"

View File

@ -1,6 +1,7 @@
{% load helpers %} {% 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 %} {% for menu, groups in nav_items %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">

View File

@ -8,6 +8,7 @@ __all__ = (
'checkmark', 'checkmark',
'copy_content', 'copy_content',
'customfield_value', 'customfield_value',
'formaction',
'tag', 'tag',
) )
@ -113,3 +114,14 @@ def htmx_table(context, viewname, return_url=None, **kwargs):
'viewname': viewname, 'viewname': viewname,
'url_params': url_params, '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'

View File

@ -1,11 +1,11 @@
from typing import Dict
from django import template from django import template
from django.template import Context from django.utils.safestring import mark_safe
from netbox.navigation.menu import MENUS from netbox.navigation.menu import MENUS
__all__ = ( __all__ = (
'nav', 'nav',
'htmx_boost',
) )
@ -13,7 +13,7 @@ register = template.Library()
@register.inclusion_tag("navigation/menu.html", takes_context=True) @register.inclusion_tag("navigation/menu.html", takes_context=True)
def nav(context: Context) -> Dict: def nav(context):
""" """
Render the navigation menu. Render the navigation menu.
""" """
@ -40,6 +40,31 @@ def nav(context: Context) -> Dict:
nav_items.append((menu, groups)) nav_items.append((menu, groups))
return { return {
"nav_items": nav_items, 'nav_items': nav_items,
"request": context["request"] '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)