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:
@@ -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();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user