fix: preserve native reorder rack devices

This commit is contained in:
2026-08-13 09:58:34 +02:00
parent a5f8cb62c7
commit 33490d40c4
5 changed files with 37 additions and 8 deletions
+4
View File
@@ -117,6 +117,10 @@ Reorder ersetzt seine nativen Gerätekacheln erst, nachdem ein vollständiger
und zur JavaScript-Version passender Datensatz geprüft wurde. Bei einer und zur JavaScript-Version passender Datensatz geprüft wurde. Bei einer
gemischten Installation aus altem Python-Code und neuen statischen Dateien gemischten Installation aus altem Python-Code und neuen statischen Dateien
bleibt deshalb die native Ansicht erhalten, statt ein leeres Rack zu zeigen. bleibt deshalb die native Ansicht erhalten, statt ein leeres Rack zu zeigen.
Ab Version `0.9.4` werden vorhandene Reorder-Kacheln außerdem direkt auf das
12-Spalten-Raster erweitert und nicht mehr vorsorglich neu erzeugt. Nur ein
zweites, von NetBox' Ein-Gerät-pro-HE-Darstellung unterschlagenes Gerät wird
aus den Plugin-Daten ergänzt. Das verhindert browserabhängige leere Racks.
Bereits vorhandene Geräte, die dieselbe HE und Rackseite belegen, aber noch Bereits vorhandene Geräte, die dieselbe HE und Rackseite belegen, aber noch
keine Plugin-Platzierungszeile besitzen, werden in Rack-SVG, Reorder und der keine Plugin-Platzierungszeile besitzen, werden in Rack-SVG, Reorder und der
Topology-Rack-Ansicht ohne Datenbankänderung gleichmäßig nebeneinander Topology-Rack-Ansicht ohne Datenbankänderung gleichmäßig nebeneinander
+1 -1
View File
@@ -1,6 +1,6 @@
from netbox.plugins import PluginConfig, get_plugin_config from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.9.3" __version__ = "0.9.4"
class NetBoxUtilitiesConfig(PluginConfig): class NetBoxUtilitiesConfig(PluginConfig):
@@ -48,6 +48,8 @@
console.warn('NetBox Utilities received no replacement devices and kept the populated native layout.'); console.warn('NetBox Utilities received no replacement devices and kept the populated native layout.');
return; return;
} }
const devicesById = new Map(data.devices.map(device => [String(device.id), device]));
const nativeItemsByFace = {};
const setAttribute = (element, name, value) => element.setAttribute(name, String(value)); const setAttribute = (element, name, value) => element.setAttribute(name, String(value));
const originalFetch = window.fetch.bind(window); const originalFetch = window.fetch.bind(window);
@@ -73,12 +75,26 @@
Object.entries(gridElements).forEach(([face, grid]) => { Object.entries(gridElements).forEach(([face, grid]) => {
setAttribute(grid, 'gs-column', columns); setAttribute(grid, 'gs-column', columns);
const nativeItemsById = new Map(
Array.from(grid.querySelectorAll('.grid-stack-item')).map(item => [item.getAttribute('gs-id'), item]),
);
grid.querySelectorAll('.grid-stack-item').forEach(item => { grid.querySelectorAll('.grid-stack-item').forEach(item => {
// The native Reorder view is based on Rack.get_rack_units(), which can // Keep every native widget whenever possible. Rack.get_rack_units() can
// expose only one device per rack unit. Rebuild the mounted grids from // expose only one device per unit, so missing shared widgets are added
// the complete server payload; keep native non-racked devices intact. // from the complete payload below.
if (face !== 'other') { if (face !== 'other') {
item.remove(); const device = devicesById.get(item.getAttribute('gs-id'));
const belongsOnFace = device && (device.face === face || device.full_depth);
if (!belongsOnFace) {
item.remove();
return;
}
setAttribute(item, 'gs-w', device.grid_width);
setAttribute(item, 'gs-h', device.grid_height);
setAttribute(item, 'gs-x', device.grid_x);
setAttribute(item, 'gs-y', device.grid_y);
item.dataset.rackWidth = String(device.width);
item.dataset.horizontalPosition = String(device.horizontal_position);
return; return;
} }
setAttribute(item, 'gs-w', columns); setAttribute(item, 'gs-w', columns);
@@ -86,6 +102,7 @@
item.dataset.rackWidth = '1'; item.dataset.rackWidth = '1';
item.dataset.horizontalPosition = '1'; item.dataset.horizontalPosition = '1';
}); });
nativeItemsByFace[face] = nativeItemsById;
}); });
const addWidget = (device, gridFace, itemFace, rearSide = false) => { const addWidget = (device, gridFace, itemFace, rearSide = false) => {
@@ -125,9 +142,14 @@
data.devices.forEach(device => { data.devices.forEach(device => {
const face = device.face === 'rear' ? 'rear' : 'front'; const face = device.face === 'rear' ? 'rear' : 'front';
addWidget(device, face, face); if (!nativeItemsByFace[face].has(String(device.id))) {
addWidget(device, face, face);
}
if (device.full_depth) { if (device.full_depth) {
addWidget(device, face === 'front' ? 'rear' : 'front', 'back', true); const oppositeFace = face === 'front' ? 'rear' : 'front';
if (!nativeItemsByFace[oppositeFace].has(String(device.id))) {
addWidget(device, oppositeFace, 'back', true);
}
} }
}); });
@@ -291,6 +291,9 @@ class ReorderRackFrontendTest(SimpleTestCase):
self.assertIn("data.schema_version !== 3", script) self.assertIn("data.schema_version !== 3", script)
self.assertIn("data.complete !== true", script) self.assertIn("data.complete !== true", script)
self.assertIn("nativeMountedItems.length", script) self.assertIn("nativeMountedItems.length", script)
self.assertIn("const nativeItemsByFace = {}", script)
self.assertIn("const device = devicesById.get", script)
self.assertIn("if (!nativeItemsByFace[face].has", script)
self.assertNotIn("partialDeviceIds", script) self.assertNotIn("partialDeviceIds", script)
self.assertNotIn("JSON.stringify", script) self.assertNotIn("JSON.stringify", script)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "netbox-utilities" name = "netbox-utilities"
version = "0.9.3" version = "0.9.4"
description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6" description = "Navigation, tenant utilities, partial-width rack devices, bulk uploads, and rack reordering for NetBox 4.6"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"