diff --git a/netbox/core/views.py b/netbox/core/views.py index 400b421d5..b19ab207b 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -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: diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 2468e9236..be3937512 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -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 diff --git a/netbox/netbox/context_processors.py b/netbox/netbox/context_processors.py index 024ca85b5..ce4f8c45e 100644 --- a/netbox/netbox/context_processors.py +++ b/netbox/netbox/context_processors.py @@ -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' } diff --git a/netbox/netbox/preferences.py b/netbox/netbox/preferences.py index 9a6fe490c..1414ea850 100644 --- a/netbox/netbox/preferences.py +++ b/netbox/netbox/preferences.py @@ -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=( diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index ba4e585ad..d609f0a18 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -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: diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 26bd7de65..616867603 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -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, }) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index fc6c18218..9678b71e3 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -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, }) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 95ac3c1b1..501149f58 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 07e7d30b4..97796e4fb 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 04cebd4c2..c6443c7df 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/htmx.ts b/netbox/project-static/src/htmx.ts index 8d92b60c8..f4092036b 100644 --- a/netbox/project-static/src/htmx.ts +++ b/netbox/project-static/src/htmx.ts @@ -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); } diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 3afaaa9fc..aa7150be7 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -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 diff --git a/netbox/project-static/styles/overrides/_bootstrap.scss b/netbox/project-static/styles/overrides/_bootstrap.scss new file mode 100644 index 000000000..f24e18890 --- /dev/null +++ b/netbox/project-static/styles/overrides/_bootstrap.scss @@ -0,0 +1,4 @@ +// Disable smooth scrolling for intra-page links +html { + scroll-behavior: auto !important; +} diff --git a/netbox/templates/account/preferences.html b/netbox/templates/account/preferences.html index c5a93c162..51f807114 100644 --- a/netbox/templates/account/preferences.html +++ b/netbox/templates/account/preferences.html @@ -6,7 +6,7 @@ {% block title %}{% trans "User Preferences" %}{% endblock %} {% block content %} -
+ {% csrf_token %} {# Built-in preferences #} diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index bb35cd3bf..acaff4295 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -15,6 +15,7 @@ + {# Page title #} {% block title %}{% trans "Home" %}{% endblock %} | NetBox diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index 071396575..931d9c886 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -58,8 +58,48 @@ Blocks: + {# User menu #} - {% include 'inc/user_menu.html' %} + {% if request.user.is_authenticated %} + + {% else %} +
+ + {% trans "Log In" %} + +
+ {% endif %} + {# /User menu #} {# Search box #} @@ -79,6 +119,7 @@ Blocks: {# Page content #}
+
{# Page header #} {% block header %} @@ -122,6 +163,8 @@ Blocks: {% endif %} {# /Bottom banner #} +
+ {# Page footer #}
@@ -173,7 +216,7 @@ Blocks: {# /Footer links #} {# Footer text #} -
    +
diff --git a/netbox/templates/dcim/moduletype/component_templates.html b/netbox/templates/dcim/moduletype/component_templates.html index cf862b2c6..d67c6e8fb 100644 --- a/netbox/templates/dcim/moduletype/component_templates.html +++ b/netbox/templates/dcim/moduletype/component_templates.html @@ -13,13 +13,13 @@