First commit
This commit is contained in:
610
public/js/managers/device-manager.js
Normal file
610
public/js/managers/device-manager.js
Normal file
@@ -0,0 +1,610 @@
|
||||
export class DeviceManager {
|
||||
constructor(layer, api, rackManager) {
|
||||
this.layer = layer;
|
||||
this.api = api;
|
||||
this.rackManager = rackManager;
|
||||
this.devices = new Map();
|
||||
this.deviceTypes = [];
|
||||
this.deviceHeight = 30;
|
||||
this.deviceSpacing = 5;
|
||||
this.deviceWidth = 500; // Physical view width
|
||||
this.currentView = 'physical'; // Track current view
|
||||
this.contextMenuHandler = null; // Store the current context menu handler
|
||||
}
|
||||
|
||||
async loadDeviceTypes() {
|
||||
try {
|
||||
this.deviceTypes = await this.api.getDeviceTypes();
|
||||
} catch (err) {
|
||||
console.error('Failed to load device types:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async loadDevices() {
|
||||
try {
|
||||
const devices = await this.api.getDevices();
|
||||
devices.forEach(deviceData => {
|
||||
this.createDeviceShape(deviceData);
|
||||
});
|
||||
this.layer.batchDraw();
|
||||
} catch (err) {
|
||||
console.error('Failed to load devices:', err);
|
||||
}
|
||||
}
|
||||
|
||||
createDeviceShape(deviceData) {
|
||||
const rackShape = this.rackManager.getRackShape(deviceData.rack_id);
|
||||
if (!rackShape) {
|
||||
console.error('Rack not found for device:', deviceData);
|
||||
return;
|
||||
}
|
||||
|
||||
const devicesContainer = rackShape.findOne('.devices-container');
|
||||
|
||||
// Convert slot position (1-42) to visual Y position
|
||||
// Slot 1 (U1) is at the bottom, slot 42 (U42) is at the top
|
||||
const rackData = this.rackManager.getRackData(deviceData.rack_id);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const maxSlots = 42;
|
||||
|
||||
// Calculate device height based on rack_units
|
||||
const rackUnits = deviceData.rack_units || 1;
|
||||
const deviceHeight = (this.deviceHeight * rackUnits) + (this.deviceSpacing * (rackUnits - 1));
|
||||
|
||||
// Calculate Y position using helper method
|
||||
const y = this.calculateDeviceY(deviceData.position, rackUnits, rackHeight);
|
||||
|
||||
const group = new Konva.Group({
|
||||
x: 10,
|
||||
y: y,
|
||||
draggable: true, // Always draggable
|
||||
id: `device-${deviceData.id}`
|
||||
});
|
||||
|
||||
// Device rectangle
|
||||
const rect = new Konva.Rect({
|
||||
width: this.deviceWidth,
|
||||
height: deviceHeight,
|
||||
fill: deviceData.color || '#4A90E2',
|
||||
stroke: '#333',
|
||||
strokeWidth: 1,
|
||||
cornerRadius: 4,
|
||||
name: 'device-rect'
|
||||
});
|
||||
|
||||
// Device name - set listening to false to let events pass through to group
|
||||
const text = new Konva.Text({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.deviceWidth,
|
||||
height: deviceHeight,
|
||||
text: deviceData.name,
|
||||
fontSize: 14,
|
||||
fontStyle: 'bold',
|
||||
fill: '#fff',
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
padding: 5,
|
||||
name: 'device-text',
|
||||
listening: false // Don't intercept events, let them pass to group
|
||||
});
|
||||
|
||||
group.add(rect);
|
||||
group.add(text);
|
||||
|
||||
// Double-click anywhere on device to rename
|
||||
group.on('dblclick', (e) => {
|
||||
e.cancelBubble = true;
|
||||
window.dispatchEvent(new CustomEvent('rename-device', {
|
||||
detail: { deviceId: deviceData.id, deviceData, deviceShape: group }
|
||||
}));
|
||||
});
|
||||
|
||||
// Drag and drop between racks
|
||||
group.on('dragstart', () => {
|
||||
// Store original parent and position
|
||||
group.setAttr('originalParent', group.getParent());
|
||||
group.setAttr('originalPosition', group.position());
|
||||
group.setAttr('originalRackId', deviceData.rack_id);
|
||||
|
||||
// Move to main layer to be on top of everything
|
||||
const absolutePos = group.getAbsolutePosition();
|
||||
group.moveTo(this.layer);
|
||||
group.setAbsolutePosition(absolutePos);
|
||||
group.moveToTop();
|
||||
group.opacity(0.7);
|
||||
});
|
||||
|
||||
group.on('dragend', async (e) => {
|
||||
group.opacity(1);
|
||||
// Pass the event to get pointer position
|
||||
await this.handleDeviceDrop(deviceData.id, group, e);
|
||||
});
|
||||
|
||||
// Right-click context menu
|
||||
group.on('contextmenu', (e) => {
|
||||
e.evt.preventDefault();
|
||||
e.cancelBubble = true; // Stop propagation to prevent rack menu
|
||||
this.showDeviceContextMenu(e, deviceData, group);
|
||||
});
|
||||
|
||||
devicesContainer.add(group);
|
||||
|
||||
// Ensure devices-container is always on top of the rack
|
||||
devicesContainer.moveToTop();
|
||||
|
||||
this.devices.set(deviceData.id, { data: deviceData, shape: group });
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
async addDevice(deviceTypeId, rackId, position, name) {
|
||||
try {
|
||||
// Generate unique name if needed
|
||||
const uniqueName = this.generateUniqueName(name);
|
||||
|
||||
const response = await this.api.createDevice(deviceTypeId, rackId, position, uniqueName);
|
||||
|
||||
// Reload devices to get full data
|
||||
const devices = await this.api.getDevices();
|
||||
const newDevice = devices.find(d => d.id === response.id);
|
||||
|
||||
if (newDevice) {
|
||||
this.createDeviceShape(newDevice);
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
return newDevice;
|
||||
} catch (err) {
|
||||
console.error('Failed to add device:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDevice(deviceId, group, suppressEvent = false) {
|
||||
try {
|
||||
await this.api.deleteDevice(deviceId);
|
||||
group.destroy();
|
||||
this.devices.delete(deviceId);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Notify table to sync (unless suppressed for bulk operations)
|
||||
if (!suppressEvent) {
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete device:', err);
|
||||
}
|
||||
}
|
||||
|
||||
showDeviceContextMenu(e, deviceData, group) {
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
const contextMenuList = document.getElementById('contextMenuList');
|
||||
|
||||
contextMenuList.innerHTML = `
|
||||
<li data-action="connect">Create Connection</li>
|
||||
<li class="divider"></li>
|
||||
<li data-action="delete">Delete Device</li>
|
||||
`;
|
||||
|
||||
contextMenu.style.left = `${e.evt.pageX}px`;
|
||||
contextMenu.style.top = `${e.evt.pageY}px`;
|
||||
contextMenu.classList.remove('hidden');
|
||||
|
||||
// Remove previous event listener if exists
|
||||
if (this.contextMenuHandler) {
|
||||
contextMenuList.removeEventListener('click', this.contextMenuHandler);
|
||||
}
|
||||
|
||||
const handleAction = async (evt) => {
|
||||
const action = evt.target.dataset.action;
|
||||
if (action === 'delete') {
|
||||
if (confirm(`Delete device ${deviceData.name}?`)) {
|
||||
this.deleteDevice(deviceData.id, group);
|
||||
}
|
||||
} else if (action === 'connect') {
|
||||
// Trigger connection creation
|
||||
window.dispatchEvent(new CustomEvent('create-connection', {
|
||||
detail: { deviceData, deviceShape: group }
|
||||
}));
|
||||
}
|
||||
contextMenu.classList.add('hidden');
|
||||
contextMenuList.removeEventListener('click', handleAction);
|
||||
this.contextMenuHandler = null;
|
||||
};
|
||||
|
||||
// Store and add the new handler
|
||||
this.contextMenuHandler = handleAction;
|
||||
contextMenuList.addEventListener('click', handleAction);
|
||||
}
|
||||
|
||||
getNextDevicePosition(rackId, requiredRackUnits = 1) {
|
||||
// Find the lowest available slot (1-42) that can fit a device with requiredRackUnits
|
||||
// U1 is at the bottom, so we fill from bottom to top
|
||||
const usedSlots = new Set();
|
||||
|
||||
// Mark ALL slots occupied by each device (accounting for rack_units)
|
||||
this.devices.forEach(device => {
|
||||
if (device.data.rack_id === rackId) {
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
// Mark all slots this device occupies
|
||||
for (let i = 0; i < rackUnits; i++) {
|
||||
usedSlots.add(device.data.position + i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find first available slot starting from U1 (bottom) that has enough consecutive space
|
||||
for (let slot = 1; slot <= 42; slot++) {
|
||||
// Check if this slot and the next (requiredRackUnits - 1) slots are all free
|
||||
let hasSpace = true;
|
||||
for (let i = 0; i < requiredRackUnits; i++) {
|
||||
if (usedSlots.has(slot + i) || (slot + i) > 42) {
|
||||
hasSpace = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSpace) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
// If no space found, return next slot after maximum (will overflow)
|
||||
return 43;
|
||||
}
|
||||
|
||||
getDeviceShape(deviceId) {
|
||||
const device = this.devices.get(deviceId);
|
||||
return device ? device.shape : null;
|
||||
}
|
||||
|
||||
getDeviceData(deviceId) {
|
||||
const device = this.devices.get(deviceId);
|
||||
return device ? device.data : null;
|
||||
}
|
||||
|
||||
getAllDevices() {
|
||||
return Array.from(this.devices.values()).map(d => d.data);
|
||||
}
|
||||
|
||||
// Calculate Y position for a device at a given slot with given rack units
|
||||
calculateDeviceY(position, rackUnits = 1, rackHeight = null) {
|
||||
const maxSlots = 42;
|
||||
|
||||
// Use same margin as left/right (10px)
|
||||
const topMargin = 10;
|
||||
|
||||
// Device at position X with N rack units occupies slots X (bottom) to X+N-1 (top)
|
||||
const topSlot = position + (rackUnits - 1);
|
||||
const visualPosition = maxSlots - topSlot;
|
||||
|
||||
return topMargin + (visualPosition * (this.deviceHeight + this.deviceSpacing));
|
||||
}
|
||||
|
||||
// Check if a device at a given position with given rack_units conflicts with other devices
|
||||
// Returns null if no conflict, or a descriptive error message if there is a conflict
|
||||
checkSlotConflict(rackId, position, rackUnits, excludeDeviceId = null) {
|
||||
const slotsOccupied = [];
|
||||
for (let i = 0; i < rackUnits; i++) {
|
||||
slotsOccupied.push(position + i);
|
||||
}
|
||||
|
||||
// Check all devices in the same rack
|
||||
const devicesInRack = Array.from(this.devices.values())
|
||||
.filter(d => d.data.rack_id === rackId && d.data.id !== excludeDeviceId);
|
||||
|
||||
for (const device of devicesInRack) {
|
||||
const deviceRackUnits = device.data.rack_units || 1;
|
||||
const deviceSlotsOccupied = [];
|
||||
for (let i = 0; i < deviceRackUnits; i++) {
|
||||
deviceSlotsOccupied.push(device.data.position + i);
|
||||
}
|
||||
|
||||
// Check for overlap
|
||||
const overlap = slotsOccupied.some(slot => deviceSlotsOccupied.includes(slot));
|
||||
if (overlap) {
|
||||
const conflictSlots = slotsOccupied.filter(slot => deviceSlotsOccupied.includes(slot));
|
||||
return `Device "${device.data.name}" already occupies slot(s) U${conflictSlots.join(', U')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null; // No conflict
|
||||
}
|
||||
|
||||
// Check if a device name already exists (case-insensitive)
|
||||
isDeviceNameTaken(name, excludeDeviceId = null) {
|
||||
const nameLower = name.toLowerCase();
|
||||
return Array.from(this.devices.values()).some(device => {
|
||||
if (excludeDeviceId && device.data.id === excludeDeviceId) {
|
||||
return false; // Exclude the device being renamed
|
||||
}
|
||||
return device.data.name.toLowerCase() === nameLower;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a unique device name by adding _XX suffix
|
||||
generateUniqueName(baseName) {
|
||||
// Remove any existing _XX suffix from the base name
|
||||
const cleanBaseName = baseName.replace(/_\d+$/, '');
|
||||
|
||||
// If the clean name is available, use it
|
||||
if (!this.isDeviceNameTaken(cleanBaseName)) {
|
||||
return cleanBaseName;
|
||||
}
|
||||
|
||||
// Find the highest existing number suffix
|
||||
let maxNumber = 0;
|
||||
const pattern = new RegExp(`^${cleanBaseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}_?(\\d+)$`, 'i');
|
||||
|
||||
Array.from(this.devices.values()).forEach(device => {
|
||||
const match = device.data.name.match(pattern);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]) || 0;
|
||||
if (num > maxNumber) {
|
||||
maxNumber = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Generate next number with padding
|
||||
const nextNumber = (maxNumber + 1).toString().padStart(2, '0');
|
||||
return `${cleanBaseName}_${nextNumber}`;
|
||||
}
|
||||
|
||||
async handleDeviceDrop(deviceId, deviceShape, event) {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (!device) return;
|
||||
|
||||
// Get the stage and mouse pointer position
|
||||
const stage = this.layer.getStage();
|
||||
const pointerPos = stage.getPointerPosition();
|
||||
|
||||
if (!pointerPos) {
|
||||
const originalParent = deviceShape.getAttr('originalParent');
|
||||
const originalPosition = deviceShape.getAttr('originalPosition');
|
||||
if (originalParent) {
|
||||
deviceShape.moveTo(originalParent);
|
||||
deviceShape.position(originalPosition);
|
||||
}
|
||||
this.layer.batchDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert pointer position from screen coordinates to world coordinates
|
||||
// Account for stage position (pan) and scale (zoom)
|
||||
const scale = stage.scaleX(); // Assumes uniform scaling (scaleX === scaleY)
|
||||
const stagePos = stage.position();
|
||||
|
||||
const worldX = (pointerPos.x - stagePos.x) / scale;
|
||||
const worldY = (pointerPos.y - stagePos.y) / scale;
|
||||
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
|
||||
// Find which rack the pointer is over
|
||||
let targetRack = null;
|
||||
let targetRackId = null;
|
||||
|
||||
// Convert Map to array to use find() instead of forEach
|
||||
const racksArray = Array.from(this.rackManager.racks.entries());
|
||||
|
||||
for (const [rackId, rack] of racksArray) {
|
||||
const rackX = rack.data.x;
|
||||
const rackY = rack.data.y;
|
||||
const rackWidth = rack.data.width || this.rackManager.rackWidth;
|
||||
const rackHeight = rack.data.height || this.rackManager.rackHeight;
|
||||
|
||||
// Check if world-space pointer is within rack bounds
|
||||
if (worldX >= rackX && worldX <= rackX + rackWidth &&
|
||||
worldY >= rackY && worldY <= rackY + rackHeight) {
|
||||
targetRack = rack;
|
||||
targetRackId = rackId;
|
||||
break; // Use first matching rack
|
||||
}
|
||||
}
|
||||
|
||||
// If not over any rack, return device to original position
|
||||
if (!targetRack) {
|
||||
const originalParent = deviceShape.getAttr('originalParent');
|
||||
const originalPosition = deviceShape.getAttr('originalPosition');
|
||||
|
||||
if (originalParent) {
|
||||
deviceShape.moveTo(originalParent);
|
||||
deviceShape.position(originalPosition);
|
||||
}
|
||||
this.layer.batchDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
const originalRackId = deviceShape.getAttr('originalRackId') || device.data.rack_id;
|
||||
|
||||
// Get the rack shape for later use
|
||||
const rackShape = targetRack.shape;
|
||||
|
||||
// Calculate position within target rack using world coordinates
|
||||
const rackY = targetRack.data.y;
|
||||
|
||||
// Use the world Y position for slot detection
|
||||
const relativeY = worldY - rackY;
|
||||
|
||||
// Convert visual Y to slot position (1-42, where U1 is at bottom)
|
||||
const maxSlots = 42;
|
||||
const slotHeight = this.deviceHeight + this.deviceSpacing;
|
||||
const topMargin = 10;
|
||||
|
||||
// Calculate which slot the pointer is in
|
||||
const visualSlotFromTop = Math.floor((relativeY - topMargin) / slotHeight);
|
||||
let newPosition = maxSlots - visualSlotFromTop; // Invert: bottom (high Y) = low slot number
|
||||
newPosition = Math.max(1, Math.min(42, newPosition)); // Clamp to 1-42
|
||||
|
||||
// Check for conflicts with existing devices in this rack
|
||||
// Note: rackUnits already declared at the beginning of this function
|
||||
const conflict = this.checkSlotConflict(targetRackId, newPosition, rackUnits, deviceId);
|
||||
|
||||
if (conflict) {
|
||||
// Position is occupied, revert to original position
|
||||
const originalParent = deviceShape.getAttr('originalParent');
|
||||
const originalPosition = deviceShape.getAttr('originalPosition');
|
||||
|
||||
if (originalParent) {
|
||||
deviceShape.moveTo(originalParent);
|
||||
deviceShape.position(originalPosition);
|
||||
}
|
||||
this.layer.batchDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
const finalPosition = newPosition;
|
||||
|
||||
// Check if device actually moved
|
||||
if (originalRackId === targetRackId && device.data.position === finalPosition) {
|
||||
// Device didn't move, but snap it back to proper slot position
|
||||
const devicesContainer = rackShape.findOne('.devices-container');
|
||||
deviceShape.moveTo(devicesContainer);
|
||||
|
||||
// Recalculate proper Y position to snap to slot
|
||||
const rackData = this.rackManager.getRackData(targetRackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const correctY = this.calculateDeviceY(finalPosition, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: correctY });
|
||||
|
||||
this.layer.batchDraw();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update device in database
|
||||
await this.api.request(`/api/devices/${deviceId}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: targetRackId, position: finalPosition })
|
||||
});
|
||||
|
||||
// Update local data
|
||||
device.data.rack_id = targetRackId;
|
||||
device.data.position = finalPosition;
|
||||
|
||||
// Move device to new rack's devices-container
|
||||
const newDevicesContainer = rackShape.findOne('.devices-container');
|
||||
deviceShape.moveTo(newDevicesContainer);
|
||||
|
||||
// Ensure devices-container is on top within the rack
|
||||
newDevicesContainer.moveToTop();
|
||||
|
||||
// Reposition device using helper method
|
||||
// Note: rackUnits already declared above
|
||||
const rackData = this.rackManager.getRackData(targetRackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.calculateDeviceY(finalPosition, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
|
||||
// NOTE: Removed auto-compacting - it was moving other devices unexpectedly
|
||||
// Users can manually adjust device positions as needed
|
||||
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Update connections after device movement
|
||||
if (this.connectionManager) {
|
||||
this.connectionManager.updateAllConnections();
|
||||
}
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to move device:', err);
|
||||
// Revert to original position
|
||||
const originalParent = deviceShape.getAttr('originalParent');
|
||||
const originalPosition = deviceShape.getAttr('originalPosition');
|
||||
|
||||
if (originalParent) {
|
||||
deviceShape.moveTo(originalParent);
|
||||
deviceShape.position(originalPosition);
|
||||
}
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
async compactRackDevices(rackId) {
|
||||
// Get all devices in this rack, sorted by position (1-42)
|
||||
const devicesInRack = Array.from(this.devices.values())
|
||||
.filter(d => d.data.rack_id === rackId)
|
||||
.sort((a, b) => a.data.position - b.data.position);
|
||||
|
||||
// Reassign positions to be sequential starting from 1 (U1 = bottom)
|
||||
const updatePromises = [];
|
||||
const maxSlots = 42;
|
||||
|
||||
devicesInRack.forEach((device, index) => {
|
||||
const newSlot = index + 1; // Slots start at 1
|
||||
|
||||
if (device.data.position !== newSlot) {
|
||||
device.data.position = newSlot;
|
||||
|
||||
// Update visual position using helper method
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.calculateDeviceY(newSlot, rackUnits, rackHeight);
|
||||
device.shape.position({ x: 10, y: newY });
|
||||
|
||||
// Update database
|
||||
updatePromises.push(
|
||||
this.api.request(`/api/devices/${device.data.id}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: rackId, position: newSlot })
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updatePromises);
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
|
||||
updateDevicesDraggability(draggable) {
|
||||
// Devices are now always draggable, regardless of rack lock state
|
||||
// This method is kept for compatibility but doesn't change draggability
|
||||
this.devices.forEach(device => {
|
||||
device.shape.draggable(true);
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentView(viewType) {
|
||||
this.currentView = viewType;
|
||||
|
||||
// Set device width based on view
|
||||
if (viewType === 'logical') {
|
||||
this.deviceWidth = 200; // Narrower in logical view
|
||||
} else {
|
||||
this.deviceWidth = 500; // Normal width in physical view
|
||||
}
|
||||
|
||||
// Resize all existing devices
|
||||
this.devices.forEach(device => {
|
||||
const rect = device.shape.findOne('.device-rect');
|
||||
const text = device.shape.findOne('.device-text');
|
||||
|
||||
// In logical view: all devices same size (1U)
|
||||
// In physical view: size based on rack units
|
||||
let deviceHeight;
|
||||
if (viewType === 'logical') {
|
||||
deviceHeight = this.deviceHeight; // All devices are 1U height in logical view
|
||||
} else {
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
deviceHeight = (this.deviceHeight * rackUnits) + (this.deviceSpacing * (rackUnits - 1));
|
||||
}
|
||||
|
||||
if (rect) {
|
||||
rect.width(this.deviceWidth);
|
||||
rect.height(deviceHeight);
|
||||
}
|
||||
if (text) {
|
||||
text.width(this.deviceWidth);
|
||||
text.height(deviceHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user