Skip to content

Commit

Permalink
Closes #14736: Enable HTMX navigation globally (#15158)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jeremystretch committed Mar 28, 2024
1 parent 04d8db7 commit 744be59
Show file tree
Hide file tree
Showing 54 changed files with 213 additions and 160 deletions.
7 changes: 4 additions & 3 deletions netbox/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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
Expand Down Expand Up @@ -320,7 +321,7 @@ def get(self, request, queue_index, status):
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,
})
Expand Down Expand Up @@ -489,8 +490,8 @@ def get(self, request, queue_index):
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:
Expand Down
3 changes: 2 additions & 1 deletion netbox/extras/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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
Expand Down Expand Up @@ -1224,7 +1225,7 @@ def get(self, request, **kwargs):
}

# 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
Expand Down
4 changes: 3 additions & 1 deletion netbox/netbox/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
8 changes: 8 additions & 0 deletions netbox/netbox/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def get_page_lengths():
),
default='light',
),
'ui.htmx_navigation': UserPreference(
label=_('HTMX Navigation'),
choices=(
('', _('Disabled')),
('true', _('Enabled')),
),
default=False
),
'locale.language': UserPreference(
label=_('Language'),
choices=(
Expand Down
5 changes: 3 additions & 2 deletions netbox/netbox/views/generic/bulk_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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
Expand Down Expand Up @@ -161,8 +162,8 @@ def get(self, request):
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:
Expand Down
7 changes: 4 additions & 3 deletions netbox/netbox/views/generic/object_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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
Expand Down Expand Up @@ -138,7 +139,7 @@ def get(self, request, *args, **kwargs):
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,
Expand Down Expand Up @@ -226,7 +227,7 @@ def get(self, request, *args, **kwargs):
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,
})
Expand Down Expand Up @@ -482,7 +483,7 @@ def get(self, request):
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,
})
Expand Down
3 changes: 2 additions & 1 deletion netbox/netbox/views/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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__ = (
Expand Down Expand Up @@ -104,7 +105,7 @@ def get(self, request):
}).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,
})
Expand Down
2 changes: 1 addition & 1 deletion netbox/project-static/dist/netbox.css

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions netbox/project-static/dist/netbox.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion netbox/project-static/dist/netbox.js.map

Large diffs are not rendered by default.

18 changes: 4 additions & 14 deletions netbox/project-static/src/htmx.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Expand All @@ -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);
}
1 change: 1 addition & 0 deletions netbox/project-static/styles/netbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions netbox/project-static/styles/overrides/_bootstrap.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Disable smooth scrolling for intra-page links
html {
scroll-behavior: auto !important;
}
2 changes: 1 addition & 1 deletion netbox/templates/account/preferences.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 #}
Expand Down
1 change: 1 addition & 0 deletions netbox/templates/base/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
47 changes: 45 additions & 2 deletions netbox/templates/base/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,48 @@ <h1 class="navbar-brand navbar-brand-autodark">
<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 #}
Expand All @@ -79,6 +119,7 @@ <h1 class="navbar-brand navbar-brand-autodark">

{# Page content #}
<div class="page-wrapper">
<div id="page-content" {% htmx_boost %}>

{# Page header #}
{% block header %}
Expand Down Expand Up @@ -122,6 +163,8 @@ <h1 class="navbar-brand navbar-brand-autodark">
{% 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">
Expand Down Expand Up @@ -173,7 +216,7 @@ <h1 class="navbar-brand navbar-brand-autodark">
{# /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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/component_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
4 changes: 2 additions & 2 deletions netbox/templates/dcim/device/components_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/consoleports.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/consoleserverports.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/frontports.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/interfaces.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/poweroutlets.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/powerports.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/dcim/device/rearports.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 744be59

Please sign in to comment.