fix: replace sidebar controls with resize handle

This commit is contained in:
2026-07-31 11:11:19 +02:00
parent 6b36184f9d
commit 5a2276aec9
12 changed files with 249 additions and 198 deletions
+8 -9
View File
@@ -32,7 +32,7 @@ Release-Tag oder ein bestimmter Commit verwendet werden:
```bash
/opt/netbox/venv/bin/pip install --upgrade --force-reinstall \
"git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.5.0"
"git+https://git.mrblake.cc/MrBlake/Netbox-Utilities.git@v0.5.1"
```
Alternativ kann hinter dem `@` die vollständige Commit-ID stehen.
@@ -202,14 +202,13 @@ Unter **Plugins > NetBox Utilities > Navigation personalisieren** sieht der
Benutzer alle Menüs, für die er aktuell Berechtigungen besitzt. Die Pfeiltasten
ändern die Reihenfolge; der Schalter blendet ein Menü aus.
Zusätzlich kann jeder Benutzer die Desktop-Navigation auf einen reinen
**Icon-Modus** einklappen und die Breite der ausgeklappten Navigation zwischen
216 und 408 Pixeln einstellen. Am unteren Rand der Navigation stehen dafür
direkte Schaltflächen für **schmaler**, **einklappen/ausklappen** und **breiter**
zur Verfügung. Änderungen über diese Schaltflächen werden sofort im
Benutzerprofil gespeichert. Im Icon-Modus öffnen sich die Menüinhalte als
seitliches Flyout; auf kleinen beziehungsweise mobilen Ansichten bleibt das
normale NetBox-Menü erhalten.
Zusätzlich kann jeder Benutzer die Desktop-Navigation direkt an der rechten
Kante zwischen 216 und 408 Pixeln breiter oder schmaler ziehen. Ein kleiner
Pfeilgriff in der Mitte dieser Kante klappt sie auf einen reinen **Icon-Modus**
ein und wieder aus. Dafür gibt es kein zusätzliches Bedienmenü. Beim Ziehen
wird die neue Breite nach dem Loslassen im Benutzerprofil gespeichert. Im
Icon-Modus öffnen sich die Menüinhalte als seitliches Flyout; auf kleinen
beziehungsweise mobilen Ansichten bleibt das normale NetBox-Menü erhalten.
**Zurücksetzen** stellt neben Reihenfolge und Sichtbarkeit auch die normale
ausgeklappte Breite von 288 Pixeln wieder her.
+3 -1
View File
@@ -1,11 +1,13 @@
from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.5.1"
class NetBoxUtilitiesConfig(PluginConfig):
name = "netbox_utilities"
verbose_name = "NetBox Utilities"
description = "Navigation, tenant filtering, bulk module installation, and atomic rack reordering"
version = "0.5.0"
version = __version__
author = "LKE"
base_url = "utilities"
min_version = "4.6.5"
+4 -9
View File
@@ -2,7 +2,6 @@ from netbox.navigation.menu import get_menus
SIDEBAR_WIDTH_MIN = 216
SIDEBAR_WIDTH_MAX = 408
SIDEBAR_WIDTH_STEP = 24
SIDEBAR_WIDTH_DEFAULT = 288
@@ -12,19 +11,15 @@ def normalize_sidebar_width(value):
except (TypeError, ValueError):
return SIDEBAR_WIDTH_DEFAULT
width = min(SIDEBAR_WIDTH_MAX, max(SIDEBAR_WIDTH_MIN, width))
steps = (width - SIDEBAR_WIDTH_MIN + SIDEBAR_WIDTH_STEP // 2) // SIDEBAR_WIDTH_STEP
return SIDEBAR_WIDTH_MIN + steps * SIDEBAR_WIDTH_STEP
return min(SIDEBAR_WIDTH_MAX, max(SIDEBAR_WIDTH_MIN, width))
def update_sidebar_layout(collapsed, width, action):
def update_sidebar_layout(collapsed, width, action, requested_width=None):
width = normalize_sidebar_width(width)
if action == "toggle":
return not collapsed, width
if action == "smaller":
return collapsed, max(SIDEBAR_WIDTH_MIN, width - SIDEBAR_WIDTH_STEP)
if action == "larger":
return collapsed, min(SIDEBAR_WIDTH_MAX, width + SIDEBAR_WIDTH_STEP)
if action == "resize":
return False, normalize_sidebar_width(requested_width)
raise ValueError("Unbekannte Navigationsaktion.")
@@ -16,10 +16,9 @@
function normalizeWidth(value) {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
const step = Number(preferences.width_step) || 24;
const requested = Number(value) || 288;
const snapped = minimum + Math.round((requested - minimum) / step) * step;
return Math.min(maximum, Math.max(minimum, snapped));
const requested = Number(value);
const width = Number.isFinite(requested) ? Math.round(requested) : 288;
return Math.min(maximum, Math.max(minimum, width));
}
function applyRootLayout() {
@@ -58,7 +57,8 @@
sidebar.querySelectorAll(':scope > li.nav-item.dropdown > button.nav-link').forEach((button) => {
if (preferences.collapsed) {
button.dataset.netboxUtilitiesTitle = 'true';
button.title = button.getAttribute('aria-label') || '';
button.title =
button.getAttribute('aria-label') || button.querySelector('.nav-link-title')?.textContent.trim() || '';
const menu = button.closest('.nav-item.dropdown')?.querySelector(':scope > .dropdown-menu.show');
if (menu) {
const top = Math.max(8, button.getBoundingClientRect().top);
@@ -94,111 +94,149 @@
});
}
function createControl(action, icon, label) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn btn-sm btn-ghost-secondary netbox-utilities-sidebar-control';
button.dataset.action = action;
button.title = label;
button.setAttribute('aria-label', label);
const iconElement = document.createElement('i');
iconElement.className = `mdi ${icon}`;
iconElement.setAttribute('aria-hidden', 'true');
button.appendChild(iconElement);
return button;
async function saveLayout(action, width) {
const body = new URLSearchParams({action});
if (width !== undefined) body.set('width', String(width));
const response = await fetch(preferences.layout_update_url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': preferences.csrf_token,
},
body,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
function updateControls(controls, sidebar, pending = false) {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
const smaller = controls.querySelector('[data-action="smaller"]');
const larger = controls.querySelector('[data-action="larger"]');
const toggle = controls.querySelector('[data-action="toggle"]');
const toggleIcon = toggle.querySelector('i');
smaller.disabled = pending || preferences.collapsed || preferences.width <= minimum;
larger.disabled = pending || preferences.collapsed || preferences.width >= maximum;
toggle.disabled = pending;
toggleIcon.className = preferences.collapsed
? 'mdi mdi-arrow-expand-right'
: 'mdi mdi-arrow-collapse-left';
const toggleLabel = preferences.collapsed ? 'Navigation ausklappen' : 'Navigation auf Icons einklappen';
toggle.title = toggleLabel;
toggle.setAttribute('aria-label', toggleLabel);
updateMenuTitles(sidebar);
}
function applyAction(action) {
const step = Number(preferences.width_step) || 24;
if (action === 'toggle') preferences.collapsed = !preferences.collapsed;
if (action === 'smaller') preferences.width = normalizeWidth(preferences.width - step);
if (action === 'larger') preferences.width = normalizeWidth(preferences.width + step);
applyRootLayout();
}
function addLayoutControls(sidebarMenu, sidebar) {
function addResizeHandle(sidebarElement, sidebar) {
if (!preferences.layout_update_url || !preferences.csrf_token) return;
const controls = document.createElement('div');
controls.className = 'netbox-utilities-sidebar-controls';
controls.setAttribute('role', 'group');
controls.setAttribute('aria-label', 'Navigationsgröße');
controls.append(
createControl('smaller', 'mdi-minus', 'Navigation schmaler'),
createControl('toggle', 'mdi-arrow-collapse-left', 'Navigation auf Icons einklappen'),
createControl('larger', 'mdi-plus', 'Navigation breiter')
);
const handle = document.createElement('div');
handle.className = 'netbox-utilities-sidebar-resizer';
handle.tabIndex = 0;
handle.setAttribute('role', 'separator');
handle.setAttribute('aria-orientation', 'vertical');
handle.setAttribute('aria-label', 'Breite der Navigation ändern');
const releaseInfo = sidebarMenu.querySelector(':scope > .text-muted');
sidebarMenu.insertBefore(controls, releaseInfo || null);
updateControls(controls, sidebar);
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'netbox-utilities-sidebar-toggle';
const toggleIcon = document.createElement('i');
toggleIcon.className = 'mdi';
toggleIcon.setAttribute('aria-hidden', 'true');
toggle.appendChild(toggleIcon);
handle.appendChild(toggle);
sidebarElement.appendChild(handle);
controls.addEventListener('click', async (event) => {
const button = event.target.closest('button[data-action]');
if (!button || button.disabled) return;
let resizing = false;
let previousLayout = null;
const previous = {collapsed: preferences.collapsed, width: preferences.width};
applyAction(button.dataset.action);
updateControls(controls, sidebar, true);
function updateHandle() {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
handle.setAttribute('aria-valuemin', String(minimum));
handle.setAttribute('aria-valuemax', String(maximum));
handle.setAttribute('aria-valuenow', String(preferences.collapsed ? minimum : preferences.width));
const label = preferences.collapsed ? 'Navigation ausklappen' : 'Navigation auf Icons einklappen';
toggle.title = label;
toggle.setAttribute('aria-label', label);
toggleIcon.className = preferences.collapsed ? 'mdi mdi-chevron-right' : 'mdi mdi-chevron-left';
updateMenuTitles(sidebar);
}
function applyWidth(clientX) {
preferences.collapsed = false;
preferences.width = normalizeWidth(clientX);
applyRootLayout();
updateHandle();
}
async function persist(action, width, fallback) {
try {
const response = await fetch(preferences.layout_update_url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': preferences.csrf_token,
},
body: new URLSearchParams({action: button.dataset.action}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const saved = await response.json();
const saved = await saveLayout(action, width);
preferences.collapsed = Boolean(saved.collapsed);
preferences.width = normalizeWidth(saved.width);
controls.classList.remove('text-danger');
controls.removeAttribute('title');
handle.classList.remove('netbox-utilities-sidebar-save-error');
handle.removeAttribute('data-save-error');
} catch (error) {
preferences.collapsed = previous.collapsed;
preferences.width = previous.width;
controls.classList.add('text-danger');
controls.title = 'Die Navigationseinstellung konnte nicht gespeichert werden.';
preferences.collapsed = fallback.collapsed;
preferences.width = fallback.width;
handle.classList.add('netbox-utilities-sidebar-save-error');
handle.dataset.saveError = 'Die Navigationseinstellung konnte nicht gespeichert werden.';
console.error('Could not save NetBox navigation layout', error);
}
applyRootLayout();
updateControls(controls, sidebar);
updateHandle();
}
toggle.addEventListener('click', async () => {
if (toggle.disabled) return;
const previous = {collapsed: preferences.collapsed, width: preferences.width};
preferences.collapsed = !preferences.collapsed;
applyRootLayout();
updateHandle();
toggle.disabled = true;
await persist('toggle', undefined, previous);
toggle.disabled = false;
});
handle.addEventListener('pointerdown', (event) => {
if (event.target.closest('button')) return;
event.preventDefault();
resizing = true;
previousLayout = {collapsed: preferences.collapsed, width: preferences.width};
handle.setPointerCapture(event.pointerId);
root.classList.add('netbox-utilities-navigation-resizing');
applyWidth(event.clientX);
});
handle.addEventListener('pointermove', (event) => {
if (resizing) applyWidth(event.clientX);
});
async function finishResize(event) {
if (!resizing) return;
resizing = false;
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
root.classList.remove('netbox-utilities-navigation-resizing');
const fallback = previousLayout;
previousLayout = null;
await persist('resize', preferences.width, fallback);
}
handle.addEventListener('pointerup', finishResize);
handle.addEventListener('pointercancel', finishResize);
handle.addEventListener('keydown', async (event) => {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
const directions = {ArrowLeft: -16, ArrowRight: 16};
let width;
if (event.key in directions) width = preferences.width + directions[event.key];
if (event.key === 'Home') width = minimum;
if (event.key === 'End') width = maximum;
if (width === undefined) return;
event.preventDefault();
const previous = {collapsed: preferences.collapsed, width: preferences.width};
applyWidth(width);
await persist('resize', preferences.width, previous);
});
updateHandle();
}
function initializeNavigation() {
const sidebarMenu = document.getElementById('sidebar-menu');
const sidebar = document.querySelector('#sidebar-menu > ul.navbar-nav');
if (!sidebarMenu || !sidebar) return;
const sidebarElement = document.querySelector('.page > aside.navbar-vertical.navbar-expand-lg');
if (!sidebarMenu || !sidebar || !sidebarElement) return;
applyMenuPreferences(sidebar);
updateMenuTitles(sidebar);
configureCollapsedDropdowns(sidebar);
addLayoutControls(sidebarMenu, sidebar);
addResizeHandle(sidebarElement, sidebar);
}
applyRootLayout();
@@ -10,7 +10,7 @@
white-space: nowrap;
}
.netbox-utilities-sidebar-controls {
.netbox-utilities-sidebar-resizer {
display: none;
}
@@ -38,24 +38,77 @@
width: var(--netbox-utilities-sidebar-effective-width);
}
.netbox-utilities-sidebar-controls {
position: sticky;
bottom: 0.5rem;
z-index: 2;
display: flex;
justify-content: center;
gap: 0.25rem;
margin: 0.75rem;
padding: 0.35rem;
border: 1px solid var(--tblr-border-color-translucent);
border-radius: var(--tblr-border-radius);
background: var(--tblr-bg-surface);
box-shadow: var(--tblr-box-shadow-sm);
.netbox-utilities-sidebar-resizer {
position: fixed;
top: 0;
bottom: 0;
left: calc(var(--netbox-utilities-sidebar-effective-width) - 5px);
z-index: 1045;
display: block;
width: 10px;
cursor: col-resize;
touch-action: none;
outline: 0;
transition: left 160ms ease;
}
.netbox-utilities-sidebar-control {
flex: 0 1 3rem;
padding-inline: 0.5rem;
.netbox-utilities-sidebar-resizer::before {
position: absolute;
top: 0;
bottom: 0;
left: 4px;
width: 2px;
content: '';
background: transparent;
transition: background-color 120ms ease;
}
.netbox-utilities-sidebar-resizer:hover::before,
.netbox-utilities-sidebar-resizer:focus-visible::before,
html.netbox-utilities-navigation-resizing .netbox-utilities-sidebar-resizer::before {
background: var(--tblr-primary);
}
.netbox-utilities-sidebar-toggle {
position: absolute;
top: 50%;
left: 50%;
display: grid;
width: 24px;
height: 44px;
padding: 0;
color: var(--tblr-secondary-color);
cursor: pointer;
background: var(--tblr-bg-surface);
border: 1px solid var(--tblr-border-color);
border-radius: 0 var(--tblr-border-radius) var(--tblr-border-radius) 0;
box-shadow: var(--tblr-box-shadow-sm);
transform: translate(0, -50%);
place-items: center;
}
.netbox-utilities-sidebar-toggle:hover,
.netbox-utilities-sidebar-toggle:focus-visible {
color: var(--tblr-primary);
border-color: var(--tblr-primary);
outline: 0;
}
.netbox-utilities-sidebar-save-error::before {
background: var(--tblr-danger) !important;
}
html.netbox-utilities-navigation-resizing,
html.netbox-utilities-navigation-resizing body {
cursor: col-resize !important;
user-select: none;
}
html.netbox-utilities-navigation-resizing .page > aside.navbar-vertical.navbar-expand-lg,
html.netbox-utilities-navigation-resizing .page > aside.navbar-vertical.navbar-expand-lg ~ .navbar,
html.netbox-utilities-navigation-resizing .page > aside.navbar-vertical.navbar-expand-lg ~ .page-wrapper,
html.netbox-utilities-navigation-resizing .netbox-utilities-sidebar-resizer {
transition: none;
}
html.netbox-utilities-navigation-collapsed .navbar-vertical.navbar-expand-lg > .container-fluid {
@@ -105,19 +158,17 @@
box-shadow: var(--tblr-box-shadow-lg);
}
html.netbox-utilities-navigation-collapsed .netbox-utilities-sidebar-controls {
margin-inline: 0;
}
html.netbox-utilities-navigation-collapsed .netbox-utilities-sidebar-control:not([data-action="toggle"]) {
display: none;
html.netbox-utilities-navigation-collapsed .netbox-utilities-sidebar-resizer {
cursor: e-resize;
}
}
@media (prefers-reduced-motion: reduce) {
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg,
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg ~ .navbar,
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg ~ .page-wrapper {
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg ~ .page-wrapper,
.netbox-utilities-sidebar-resizer,
.netbox-utilities-sidebar-resizer::before {
transition: none;
}
}
+2 -2
View File
@@ -4,12 +4,12 @@ from django.urls import reverse
from netbox.plugins import PluginTemplateExtension
from tenancy.models import Tenant, TenantGroup
from . import __version__
from .models import NavigationPreference
from .navigation_helpers import (
SIDEBAR_WIDTH_DEFAULT,
SIDEBAR_WIDTH_MAX,
SIDEBAR_WIDTH_MIN,
SIDEBAR_WIDTH_STEP,
get_visible_menus,
normalize_preferences,
normalize_sidebar_width,
@@ -43,7 +43,6 @@ class UtilitiesGlobalContent(PluginTemplateExtension):
"width": normalize_sidebar_width(preference.sidebar_width if preference else SIDEBAR_WIDTH_DEFAULT),
"width_min": SIDEBAR_WIDTH_MIN,
"width_max": SIDEBAR_WIDTH_MAX,
"width_step": SIDEBAR_WIDTH_STEP,
"layout_update_url": reverse("plugins:netbox_utilities:navigation_layout"),
"csrf_token": get_token(request),
}
@@ -52,6 +51,7 @@ class UtilitiesGlobalContent(PluginTemplateExtension):
{
"navigation_enabled": request.user.is_authenticated and navigation_customization_enabled(),
"navigation_preferences": preference_data,
"asset_version": __version__,
},
)
@@ -1,6 +1,6 @@
{% load static %}
<link rel="stylesheet" href="{% static 'netbox_utilities/netbox_utilities.css' %}">
<link rel="stylesheet" href="{% static 'netbox_utilities/netbox_utilities.css' %}?v={{ asset_version }}">
{% if navigation_enabled %}
{{ navigation_preferences|json_script:"netbox-utilities-navigation-data" }}
<script src="{% static 'netbox_utilities/navigation.js' %}" defer></script>
<script src="{% static 'netbox_utilities/navigation.js' %}?v={{ asset_version }}" defer></script>
{% endif %}
@@ -22,46 +22,11 @@
</div>
{% else %}
<p class="text-secondary">
Passe Breite und Darstellung an, verschiebe Menüs mit den Pfeilen und blende nicht benötigte Bereiche aus.
Verschiebe Menüs mit den Pfeilen und blende nicht benötigte Bereiche aus.
Berechtigungen von NetBox werden dadurch nicht verändert.
</p>
<form method="post" id="navigation-preferences-form">
{% csrf_token %}
<fieldset class="border rounded p-3 mb-4">
<legend class="float-none w-auto px-2 fs-4 mb-2">Darstellung</legend>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="sidebar-collapsed"
name="sidebar_collapsed"
{% if sidebar_collapsed %}checked{% endif %}
>
<label class="form-check-label fw-medium" for="sidebar-collapsed">
Eingeklappt nur Hauptmenü-Icons anzeigen
</label>
</div>
<label class="form-label fw-medium" for="sidebar-width">
Breite im ausgeklappten Zustand:
<output id="sidebar-width-output" for="sidebar-width">{{ sidebar_width }} px</output>
</label>
<input
class="form-range"
type="range"
id="sidebar-width"
name="sidebar_width"
min="{{ sidebar_width_min }}"
max="{{ sidebar_width_max }}"
step="{{ sidebar_width_step }}"
value="{{ sidebar_width }}"
>
<div class="d-flex justify-content-between text-secondary small">
<span>Kleiner</span>
<span>Standard: 288 px</span>
<span>Größer</span>
</div>
</fieldset>
<div class="list-group mb-3" id="navigation-menu-editor">
{% for menu in menus %}
<div class="list-group-item d-flex align-items-center gap-3 navigation-menu-row">
@@ -119,14 +84,6 @@
}
});
}
const width = document.getElementById('sidebar-width');
const output = document.getElementById('sidebar-width-output');
if (width && output) {
width.addEventListener('input', () => {
output.value = `${width.value} px`;
});
}
})();
</script>
{% endblock content %}
@@ -29,11 +29,11 @@ class NormalizePreferencesTest(SimpleTestCase):
class SidebarLayoutTest(SimpleTestCase):
def test_normalizes_width_to_supported_steps(self):
def test_clamps_width_without_discrete_steps(self):
self.assertEqual(normalize_sidebar_width(None), SIDEBAR_WIDTH_DEFAULT)
self.assertEqual(normalize_sidebar_width(100), SIDEBAR_WIDTH_MIN)
self.assertEqual(normalize_sidebar_width(500), SIDEBAR_WIDTH_MAX)
self.assertEqual(normalize_sidebar_width(300), 312)
self.assertEqual(normalize_sidebar_width(300), 300)
def test_toggles_icon_only_mode(self):
collapsed, width = update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "toggle")
@@ -41,12 +41,15 @@ class SidebarLayoutTest(SimpleTestCase):
self.assertTrue(collapsed)
self.assertEqual(width, SIDEBAR_WIDTH_DEFAULT)
def test_resizes_within_limits(self):
self.assertEqual(update_sidebar_layout(False, SIDEBAR_WIDTH_MIN, "smaller")[1], SIDEBAR_WIDTH_MIN)
self.assertEqual(update_sidebar_layout(False, SIDEBAR_WIDTH_MAX, "larger")[1], SIDEBAR_WIDTH_MAX)
self.assertLess(
update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "smaller")[1],
SIDEBAR_WIDTH_DEFAULT,
def test_resizes_continuously_within_limits_and_expands(self):
self.assertEqual(update_sidebar_layout(True, SIDEBAR_WIDTH_DEFAULT, "resize", 301), (False, 301))
self.assertEqual(
update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "resize", 100),
(False, SIDEBAR_WIDTH_MIN),
)
self.assertEqual(
update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "resize", 500),
(False, SIDEBAR_WIDTH_MAX),
)
def test_rejects_unknown_action(self):
@@ -25,6 +25,24 @@ class NavigationLayoutViewTest(SimpleTestCase):
self.assertTrue(preference.sidebar_collapsed)
preference.save.assert_called_once_with(update_fields=("sidebar_collapsed", "sidebar_width", "updated"))
@patch("netbox_utilities.views.navigation_customization_enabled", return_value=True)
@patch("netbox_utilities.views.NavigationPreference.objects.get_or_create")
def test_saves_continuously_resized_width(self, get_or_create, _feature_enabled):
preference = MagicMock(sidebar_collapsed=True, sidebar_width=288)
get_or_create.return_value = (preference, False)
request = self.factory.post(
"/plugins/utilities/navigation/layout/",
{"action": "resize", "width": "317"},
)
request.user = MagicMock()
response = NavigationLayoutView().post(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), {"collapsed": False, "width": 317})
self.assertFalse(preference.sidebar_collapsed)
self.assertEqual(preference.sidebar_width, 317)
@patch("netbox_utilities.views.navigation_customization_enabled", return_value=True)
@patch("netbox_utilities.views.NavigationPreference.objects.get_or_create")
def test_rejects_unknown_layout_action(self, get_or_create, _feature_enabled):
+1 -13
View File
@@ -20,12 +20,8 @@ from .models import NavigationPreference, UtilitiesSettings
from .module_installation import BulkModuleInstallError, install_modules
from .navigation_helpers import (
SIDEBAR_WIDTH_DEFAULT,
SIDEBAR_WIDTH_MAX,
SIDEBAR_WIDTH_MIN,
SIDEBAR_WIDTH_STEP,
get_visible_menus,
normalize_preferences,
normalize_sidebar_width,
update_sidebar_layout,
)
from .runtime import (
@@ -137,8 +133,6 @@ class NavigationPreferencesView(LoginRequiredMixin, View):
)
preference.menu_order = order
preference.hidden_menus = hidden
preference.sidebar_collapsed = request.POST.get("sidebar_collapsed") == "on"
preference.sidebar_width = normalize_sidebar_width(request.POST.get("sidebar_width"))
preference.save()
messages.success(request, "Die persönliche Navigation wurde gespeichert.")
return redirect("plugins:netbox_utilities:navigation_preferences")
@@ -159,13 +153,6 @@ class NavigationPreferencesView(LoginRequiredMixin, View):
{
"menus": menus,
"feature_enabled": navigation_customization_enabled(),
"sidebar_collapsed": preference.sidebar_collapsed if preference else False,
"sidebar_width": normalize_sidebar_width(
preference.sidebar_width if preference else SIDEBAR_WIDTH_DEFAULT
),
"sidebar_width_min": SIDEBAR_WIDTH_MIN,
"sidebar_width_max": SIDEBAR_WIDTH_MAX,
"sidebar_width_step": SIDEBAR_WIDTH_STEP,
},
)
@@ -183,6 +170,7 @@ class NavigationLayoutView(LoginRequiredMixin, View):
preference.sidebar_collapsed,
preference.sidebar_width,
request.POST.get("action"),
request.POST.get("width"),
)
except ValueError as error:
return HttpResponseBadRequest(str(error))
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "netbox-utilities"
version = "0.5.0"
version = "0.5.1"
description = "Navigation, tenant filtering, bulk module installation, and atomic rack reordering for NetBox 4.6"
readme = "README.md"
requires-python = ">=3.12"