Add images/labels toggle, clean up label styling, merge real racks

- Replace the device-image label background bar and text-shadow with
  NetBox's own technique: a black-stroke pass behind a white-fill pass
  of the same text, giving crisp readable labels over any image
  without a dark box behind them.
- Add an "Images and labels / Images only / Labels only" selector
  above the elevation, mirroring the equivalent control on real NetBox
  rack elevations. The plain label is always in the markup; the
  selector only toggles the image and its over-image label, exactly
  like core NetBox does it. Choice persists per browser via
  localStorage and works without edit permissions.
- Add a "Merge into concept" button to the real dcim Rack list: select
  two racks via the existing bulk-select checkboxes and it forwards
  them to the concept-merge page, pre-filled. New
  deploy.merge_racks_to_concept() combines two real racks' devices
  directly into one new, stacked blueprint without an intermediate
  per-rack copy step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 09:50:23 +02:00
co-authored by Claude Sonnet 5
parent 777ded3900
commit e4817ea83b
13 changed files with 430 additions and 39 deletions
@@ -41,19 +41,32 @@
pointer-events: none;
}
.concept-elevation .device-label-bg {
fill: #000000;
fill-opacity: 0.45;
pointer-events: none;
}
.concept-elevation .device-label {
font-size: 11px;
pointer-events: none;
}
.concept-elevation .device-label.on-image {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
/* Label drawn over an image: a black stroke pass behind a white fill pass gives crisp,
readable text on any picture without a solid backing box. */
.concept-elevation .device-image-label {
font-size: 11px;
pointer-events: none;
}
.concept-elevation .device-image-label-stroke {
fill: none;
stroke: #000000;
stroke-width: 0.2em;
stroke-linejoin: round;
}
.concept-elevation .device-image-label-fill {
fill: #ffffff;
}
/* Toggled by the "images and labels / images only / labels only" selector. */
.concept-elevation .hidden {
visibility: hidden;
}
.concept-elevation .opposite-hint {
@@ -1,3 +1,50 @@
/*
* View selector: show/hide device images and their labels, mirroring the
* "images-and-labels / images-only / labels-only" control on NetBox's own rack
* elevation. The plain (non-image) label is never toggled by this - it's always in the
* markup, and an opaque image simply sits on top of it, exactly as NetBox does it.
* Runs independently of edit permissions, so it works for read-only users too.
*/
(function () {
'use strict';
const STORAGE_KEY = 'netbox_rack_concept:elevation-view';
const select = document.getElementById('concept-elevation-view');
const container = document.getElementById('concept-elevations');
if (!select || !container) {
return;
}
function applyView(view) {
const showImages = view !== 'labels-only';
const showImageLabels = view === 'images-and-labels';
container.querySelectorAll('image.device-image').forEach(function (el) {
el.classList.toggle('hidden', !showImages);
});
container.querySelectorAll('.device-image-label').forEach(function (el) {
el.classList.toggle('hidden', !showImageLabels);
});
}
let initial = 'images-and-labels';
try {
initial = window.localStorage.getItem(STORAGE_KEY) || initial;
} catch (e) {
// Storage may be unavailable (private browsing, blocked cookies); fall back silently.
}
select.value = initial;
applyView(initial);
select.addEventListener('change', function () {
applyView(select.value);
try {
window.localStorage.setItem(STORAGE_KEY, select.value);
} catch (e) {
// Nothing to do if storage isn't available - the choice just won't persist.
}
});
})();
/*
* Drag & drop for concept rack elevations.
*
@@ -0,0 +1,49 @@
/*
* "Merge into concept" button on the real dcim Rack list.
*
* The button lives outside NetBox's own bulk-action <form> (plugin list buttons render
* in the page header, the row checkboxes further down in the table's form), so it
* can't rely on a normal form submit to collect the selection. Instead it reads the
* checked "pk" checkboxes straight out of the DOM - the same input name NetBox's own
* bulk-edit/bulk-delete buttons use - and forwards up to two of them as query
* parameters to the merge page, which pre-fills its two rack fields from that.
*/
(function () {
'use strict';
const button = document.getElementById('concept-merge-racks-button');
if (!button) {
return;
}
function checkedRackIds() {
return Array.prototype.map.call(
document.querySelectorAll('input[name="pk"]:checked'),
function (el) { return el.value; }
);
}
button.addEventListener('click', function (event) {
const ids = checkedRackIds();
if (ids.length === 0) {
// Nothing selected: fall through to the plain link, let the user pick
// both racks by hand on the merge page instead.
return;
}
if (ids.length > 2) {
event.preventDefault();
window.alert(
'Please select exactly two racks to merge (you selected ' + ids.length + ').'
);
return;
}
event.preventDefault();
const url = new URL(button.dataset.mergeUrl, window.location.origin);
url.searchParams.set('rack_bottom', ids[0]);
if (ids.length === 2) {
url.searchParams.set('rack_top', ids[1]);
}
window.location.href = url.toString();
});
})();