fix: preserve reorder drag width and images

This commit is contained in:
2026-08-13 16:10:47 +02:00
parent 79a071597c
commit 839b076d94
8 changed files with 88 additions and 37 deletions
+6
View File
@@ -138,6 +138,12 @@ GridStack-11-Stylesheet von NetBox 4.6.7, durch die Gerätekacheln mit einer
berechneten Breite von `0px` unsichtbar waren. Topology-Breiten werden außerdem berechneten Breite von `0px` unsichtbar waren. Topology-Breiten werden außerdem
nach dem vollständigen Rendern zentral auf die HTTP-Antwort angewendet und nach dem vollständigen Rendern zentral auf die HTTP-Antwort angewendet und
zusätzlich direkt in die vorhandenen Geräte-Styles geschrieben. zusätzlich direkt in die vorhandenen Geräte-Styles geschrieben.
Ab Version `0.9.8` behalten Geräte beim Ziehen in Reorder ihre tatsächliche
Rackbreite. Die 12-Spalten-Regeln lassen dazu GridStacks temporären
Pixelkoordinaten während des Ziehens Vorrang, statt eine Prozentbreite auf das
Browserfenster anzuwenden. Die serverseitig aufgebauten Reorder-Kacheln zeigen
außerdem wieder die Front- und Rückseitenbilder des Gerätetyps entsprechend
der gewählten Ansicht an.
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.7" __version__ = "0.9.8"
class NetBoxUtilitiesConfig(PluginConfig): class NetBoxUtilitiesConfig(PluginConfig):
@@ -41,35 +41,38 @@
* provides GridStack 11's base stylesheet. GridStack 11 calculates item * provides GridStack 11's base stylesheet. GridStack 11 calculates item
* widths through a CSS variable which the older runtime never sets. Without * widths through a CSS variable which the older runtime never sets. Without
* these explicit rules every 12-column widget therefore has width: 0px. * these explicit rules every 12-column widget therefore has width: 0px.
* Do not mark these declarations important: while dragging, GridStack writes
* pixel coordinates inline on a fixed-position helper. Those inline values
* must take precedence or the helper is sized relative to the viewport.
*/ */
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item { .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item {
width: calc(100% / 12) !important; width: calc(100% / 12);
} }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="2"] { width: calc(200% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="2"] { width: calc(200% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="3"] { width: 25% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="3"] { width: 25%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="4"] { width: calc(400% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="4"] { width: calc(400% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="5"] { width: calc(500% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="5"] { width: calc(500% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="6"] { width: 50% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="6"] { width: 50%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="7"] { width: calc(700% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="7"] { width: calc(700% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="8"] { width: calc(800% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="8"] { width: calc(800% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="9"] { width: 75% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="9"] { width: 75%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="10"] { width: calc(1000% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="10"] { width: calc(1000% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="11"] { width: calc(1100% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="11"] { width: calc(1100% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="12"] { width: 100% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-w="12"] { width: 100%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="0"] { left: 0 !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="0"] { left: 0; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="1"] { left: calc(100% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="1"] { left: calc(100% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="2"] { left: calc(200% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="2"] { left: calc(200% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="3"] { left: 25% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="3"] { left: 25%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="4"] { left: calc(400% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="4"] { left: calc(400% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="5"] { left: calc(500% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="5"] { left: calc(500% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="6"] { left: 50% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="6"] { left: 50%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="7"] { left: calc(700% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="7"] { left: calc(700% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="8"] { left: calc(800% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="8"] { left: calc(800% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="9"] { left: 75% !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="9"] { left: 75%; }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="10"] { left: calc(1000% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="10"] { left: calc(1000% / 12); }
.netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="11"] { left: calc(1100% / 12) !important; } .netbox-utilities-reorder-grid[gs-column="12"] > .grid-stack-item[gs-x="11"] { left: calc(1100% / 12); }
.netbox-utilities-sidebar-resizer { .netbox-utilities-sidebar-resizer {
display: none; display: none;
@@ -45,8 +45,8 @@
gs-locked="{% if not request.user|can_change:device %}true{% else %}false{% endif %}" gs-locked="{% if not request.user|can_change:device %}true{% else %}false{% endif %}"
gs-no-move="{% if not request.user|can_change:device %}true{% else %}false{% endif %}"> gs-no-move="{% if not request.user|can_change:device %}true{% else %}false{% endif %}">
<div class="grid-stack-item-content" <div class="grid-stack-item-content"
style="background-color: #{{ device.role.color|default:'1685fc' }}; color: #{% if device.role.color %}{{ device.role.color|text_color }}{% else %}000000{% endif %}"> style="{% if images and device.device_type.front_image %}background-image: url('{{ device.device_type.front_image.url }}'); background-size: {{ unit_width }}px;{% else %}background-color: #{{ device.role.color|default:'1685fc' }};{% endif %} color: #{% if device.role.color %}{{ device.role.color|text_color }}{% else %}000000{% endif %}">
{{ device.name|default:device.device_type }} {% if images and device.device_type.front_image %}{% if labels %}{{ device.name|default:device.device_type }}{% endif %}{% else %}{{ device.name|default:device.device_type }}{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@@ -20,10 +20,17 @@
data-item-face="{% if device.face == face %}{{ face }}{% else %}back{% endif %}" data-item-face="{% if device.face == face %}{{ face }}{% else %}back{% endif %}"
data-rack-width="{{ device.width }}" data-rack-width="{{ device.width }}"
data-horizontal-position="{{ device.horizontal_position }}"> data-horizontal-position="{{ device.horizontal_position }}">
<div class="grid-stack-item-content{% if device.face != face %} device_rear{% endif %}" {% if device.face == face %}
style="{% if device.face == face %}background-color: #{{ device.color }}; color: #{{ device.text_color }}{% else %}color: #000000{% endif %}"> <div class="grid-stack-item-content"
{{ device.label }} style="{% if images and device.front_image %}background-image: url('{{ device.front_image }}'); background-size: {{ unit_width }}px; color: #{{ device.text_color }}{% else %}background-color: #{{ device.color }}; color: #{{ device.text_color }}{% endif %}">
</div> {% if images and device.front_image %}{% if labels %}{{ device.label }}{% endif %}{% else %}{{ device.label }}{% endif %}
</div>
{% else %}
<div class="grid-stack-item-content device_rear"
style="{% if images and device.rear_image %}background-image: url('{{ device.rear_image }}'); background-size: {{ unit_width }}px; color: #{{ device.text_color }}{% else %}color: #000000{% endif %}">
{% if images and device.rear_image %}{% if labels %}{{ device.label }}{% endif %}{% else %}{{ device.label }}{% endif %}
</div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
+39 -4
View File
@@ -348,9 +348,44 @@ class ReorderRackFrontendTest(SimpleTestCase):
encoding="utf-8" encoding="utf-8"
) )
self.assertIn('.netbox-utilities-reorder-grid[gs-column="12"]', stylesheet) self.assertIn('.netbox-utilities-reorder-grid[gs-column="12"]', stylesheet)
self.assertIn('.grid-stack-item[gs-w="6"] { width: 50% !important; }', stylesheet) self.assertIn('.grid-stack-item[gs-w="6"] { width: 50%; }', stylesheet)
self.assertIn('.grid-stack-item[gs-w="12"] { width: 100% !important; }', stylesheet) self.assertIn('.grid-stack-item[gs-w="12"] { width: 100%; }', stylesheet)
self.assertIn('.grid-stack-item[gs-x="6"] { left: 50% !important; }', stylesheet) self.assertIn('.grid-stack-item[gs-x="6"] { left: 50%; }', stylesheet)
self.assertNotIn('.grid-stack-item[gs-w="6"] { width: 50% !important; }', stylesheet)
rendered_image_grid = render_to_string(
"netbox_utilities/reorder_rack_grid.html",
{
"object": SimpleNamespace(units=[1], u_height=42),
"face": "front",
"grid_id": 0,
"images": True,
"labels": False,
"unit_width": 220,
"reorder_devices": [
{
"id": 10,
"label": "LEO-Fritzbox",
"face": "front",
"full_depth": False,
"grid_x": 0,
"grid_y": 62,
"grid_width": 6,
"grid_height": 2,
"width": 2,
"horizontal_position": 1,
"color": "f0a000",
"text_color": "000000",
"locked": False,
"front_image": "/media/device-type-images/fritzbox.png",
"rear_image": None,
}
],
},
)
self.assertIn("background-image: url('/media/device-type-images/fritzbox.png')", rendered_image_grid)
self.assertIn("background-size: 220px", rendered_image_grid)
self.assertNotIn(">LEO-Fritzbox<", rendered_image_grid.replace("\n", ""))
def test_resolved_reorder_page_uses_server_side_width_view(self): def test_resolved_reorder_page_uses_server_side_width_view(self):
view_class = resolve(reverse("dcim:rack_reorder", kwargs={"pk": 3})).func.view_class view_class = resolve(reverse("dcim:rack_reorder", kwargs={"pk": 3})).func.view_class
@@ -414,7 +449,7 @@ class ReorderRackFrontendTest(SimpleTestCase):
self.assertEqual(template_name, "netbox_utilities/reorder_rack.html") self.assertEqual(template_name, "netbox_utilities/reorder_rack.html")
self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox") self.assertEqual(context["reorder_devices"][0]["label"], "LEO-Fritzbox")
self.assertEqual(context["reorder_devices"][0]["grid_width"], 6) self.assertEqual(context["reorder_devices"][0]["grid_width"], 6)
self.assertEqual(context["asset_version"], "0.9.7") self.assertEqual(context["asset_version"], "0.9.8")
self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value) self.assertIs(context["reorder_rack_width_data"], get_width_data.return_value)
get_width_data.assert_called_once() get_width_data.assert_called_once()
self.assertIs(get_width_data.call_args.kwargs["rack"], rack) self.assertIs(get_width_data.call_args.kwargs["rack"], rack)
@@ -246,7 +246,7 @@ class TopologyViewsRackWidthTest(SimpleTestCase):
self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html) self.assertIn('id="netbox-utilities-topology-rack-width-styles"', html)
self.assertIn('.rack-device[href="/dcim/devices/334/"]', html) self.assertIn('.rack-device[href="/dcim/devices/334/"]', html)
self.assertIn("left: calc(50% + 3px) !important", html) self.assertIn("left: calc(50% + 3px) !important", html)
self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.7", html) self.assertIn("netbox_utilities/topology-rack-width.js?v=0.9.8", html)
self.assertIn("left:calc(0% + 3px)!important", html) self.assertIn("left:calc(0% + 3px)!important", html)
self.assertIn("left:calc(50% + 3px)!important", html) self.assertIn("left:calc(50% + 3px)!important", html)
self.assertIn("width:calc(50% - 6px)!important", html) self.assertIn("width:calc(50% - 6px)!important", html)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "netbox-utilities" name = "netbox-utilities"
version = "0.9.7" version = "0.9.8"
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"