189 lines
7.6 KiB
JavaScript
189 lines
7.6 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
const source = document.getElementById('netbox-utilities-reorder-rack-width-data');
|
|
if (!source) return;
|
|
|
|
let data;
|
|
try {
|
|
data = JSON.parse(source.textContent);
|
|
} catch (error) {
|
|
console.error('NetBox Utilities could not read the Reorder Rack width data.', error);
|
|
return;
|
|
}
|
|
if (data.status && data.status !== 'ready') {
|
|
console.warn('NetBox Utilities is leaving Reorder Rack in its native layout.', data.status);
|
|
return;
|
|
}
|
|
if (data.schema_version !== 3 || data.complete !== true || !Array.isArray(data.devices)) {
|
|
console.warn('NetBox Utilities received an incomplete Reorder Rack payload and kept the native layout.');
|
|
return;
|
|
}
|
|
const validDeviceData = data.devices.every(device => (
|
|
Number.isInteger(Number(device.id))
|
|
&& ['front', 'rear'].includes(device.face)
|
|
&& Number.isInteger(Number(device.grid_x))
|
|
&& Number.isInteger(Number(device.grid_y))
|
|
&& Number.isInteger(Number(device.grid_width))
|
|
&& Number.isInteger(Number(device.grid_height))
|
|
));
|
|
if (!validDeviceData) {
|
|
console.warn('NetBox Utilities received invalid Reorder Rack device data and kept the native layout.');
|
|
return;
|
|
}
|
|
|
|
const columns = Number.parseInt(data.columns, 10) || 12;
|
|
const gridElements = {
|
|
front: document.getElementById('grid-front'),
|
|
rear: document.getElementById('grid-rear'),
|
|
other: document.getElementById('grid-other'),
|
|
};
|
|
if (!gridElements.front || !gridElements.rear || !gridElements.other) return;
|
|
|
|
const nativeMountedItems = [
|
|
...gridElements.front.querySelectorAll('.grid-stack-item'),
|
|
...gridElements.rear.querySelectorAll('.grid-stack-item'),
|
|
];
|
|
if (!data.devices.length && nativeMountedItems.length) {
|
|
console.warn('NetBox Utilities received no replacement devices and kept the populated native layout.');
|
|
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 originalFetch = window.fetch.bind(window);
|
|
window.fetch = (resource, options = {}) => {
|
|
const resourceUrl = typeof resource === 'string' || resource instanceof URL
|
|
? resource
|
|
: resource?.url;
|
|
let url;
|
|
try {
|
|
url = new URL(resourceUrl, document.baseURI);
|
|
} catch (_) {
|
|
return originalFetch(resource, options);
|
|
}
|
|
const method = String(options.method || resource?.method || 'GET').toUpperCase();
|
|
if (method !== 'PUT' || !/\/api\/plugins\/reorder\/save\/\d+\/?$/.test(url.pathname)) {
|
|
return originalFetch(resource, options);
|
|
}
|
|
|
|
const headers = new Headers(options.headers || resource?.headers);
|
|
headers.set('X-NetBox-Utilities-Rack-Grid-Columns', String(columns));
|
|
return originalFetch(resource, {...options, headers});
|
|
};
|
|
|
|
Object.entries(gridElements).forEach(([face, grid]) => {
|
|
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 => {
|
|
// Keep every native widget whenever possible. Rack.get_rack_units() can
|
|
// expose only one device per unit, so missing shared widgets are added
|
|
// from the complete payload below.
|
|
if (face !== 'other') {
|
|
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;
|
|
}
|
|
setAttribute(item, 'gs-w', columns);
|
|
setAttribute(item, 'gs-x', 0);
|
|
item.dataset.rackWidth = '1';
|
|
item.dataset.horizontalPosition = '1';
|
|
});
|
|
nativeItemsByFace[face] = nativeItemsById;
|
|
});
|
|
|
|
const addWidget = (device, gridFace, itemFace, rearSide = false) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'grid-stack-item netbox-utilities-reorder-rack-device';
|
|
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);
|
|
setAttribute(item, 'gs-id', device.id);
|
|
setAttribute(item, 'gs-locked', device.locked ? 'true' : 'false');
|
|
setAttribute(item, 'gs-no-move', device.locked ? 'true' : 'false');
|
|
item.dataset.itemColor = device.color;
|
|
item.dataset.itemTextColor = device.text_color;
|
|
item.dataset.fullDepth = device.full_depth ? 'True' : 'False';
|
|
item.dataset.itemFace = itemFace;
|
|
item.dataset.rackWidth = String(device.width);
|
|
item.dataset.horizontalPosition = String(device.horizontal_position);
|
|
|
|
const content = document.createElement('div');
|
|
content.className = 'grid-stack-item-content';
|
|
if (rearSide) content.classList.add('device_rear');
|
|
const image = rearSide ? device.rear_image : device.front_image;
|
|
if (image && data.images) {
|
|
content.style.backgroundImage = `url("${String(image).replaceAll('"', '\\"')}")`;
|
|
content.style.backgroundSize = `${data.unit_width}px`;
|
|
content.style.color = `#${device.text_color}`;
|
|
if (data.labels) content.textContent = device.label;
|
|
} else {
|
|
if (!rearSide) content.style.backgroundColor = `#${device.color}`;
|
|
content.style.color = rearSide ? '#000000' : `#${device.text_color}`;
|
|
content.textContent = device.label;
|
|
}
|
|
item.appendChild(content);
|
|
gridElements[gridFace].appendChild(item);
|
|
};
|
|
|
|
data.devices.forEach(device => {
|
|
const face = device.face === 'rear' ? 'rear' : 'front';
|
|
if (!nativeItemsByFace[face].has(String(device.id))) {
|
|
addWidget(device, face, face);
|
|
}
|
|
if (device.full_depth) {
|
|
const oppositeFace = face === 'front' ? 'rear' : 'front';
|
|
if (!nativeItemsByFace[oppositeFace].has(String(device.id))) {
|
|
addWidget(device, oppositeFace, 'back', true);
|
|
}
|
|
}
|
|
});
|
|
|
|
const snapElement = element => {
|
|
if (!element?.gridstackNode) return;
|
|
const width = Number.parseInt(element.dataset.rackWidth || '1', 10);
|
|
if (![1, 2, 3, 4].includes(width)) return;
|
|
const gridWidth = columns / width;
|
|
const node = element.gridstackNode;
|
|
const gridX = Math.max(0, Math.min(columns - gridWidth, Math.round(node.x / gridWidth) * gridWidth));
|
|
element.dataset.horizontalPosition = String(gridX / gridWidth + 1);
|
|
if (node.x !== gridX || node.w !== gridWidth) {
|
|
node.grid.update(element, {x: gridX, w: gridWidth});
|
|
}
|
|
};
|
|
|
|
const snapGrid = grid => grid.getGridItems().forEach(snapElement);
|
|
const attachSnapping = () => {
|
|
Object.values(gridElements).forEach(element => {
|
|
const grid = element.gridstack;
|
|
if (!grid) return;
|
|
grid.on('dragstop', (_event, item) => snapElement(item));
|
|
grid.on('dropped', (_event, _previous, current) => snapElement(current?.el));
|
|
});
|
|
};
|
|
|
|
document.addEventListener('click', event => {
|
|
const target = event.target instanceof Element ? event.target : event.target?.parentElement;
|
|
if (!target?.closest('#saveButton')) return;
|
|
Object.values(gridElements).forEach(element => {
|
|
if (element.gridstack) snapGrid(element.gridstack);
|
|
});
|
|
}, true);
|
|
|
|
document.addEventListener('DOMContentLoaded', attachSnapping, {once: true});
|
|
})();
|