First commit
This commit is contained in:
513
archive/old_public/js/device-manager.js
Normal file
513
archive/old_public/js/device-manager.js
Normal file
@@ -0,0 +1,513 @@
|
||||
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
|
||||
}
|
||||
|
||||
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: !this.rackManager.racksLocked, // Draggable when racks are unlocked
|
||||
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
|
||||
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'
|
||||
});
|
||||
|
||||
// Make name clickable for renaming
|
||||
text.on('click', (e) => {
|
||||
e.cancelBubble = true; // Prevent group drag
|
||||
window.dispatchEvent(new CustomEvent('rename-device', {
|
||||
detail: { deviceId: deviceData.id, deviceData, deviceShape: group }
|
||||
}));
|
||||
});
|
||||
|
||||
text.on('mouseenter', () => {
|
||||
document.body.style.cursor = 'text';
|
||||
text.fontStyle('bold italic');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
text.on('mouseleave', () => {
|
||||
document.body.style.cursor = 'default';
|
||||
text.fontStyle('bold');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
group.add(rect);
|
||||
group.add(text);
|
||||
|
||||
// Drag and drop between racks
|
||||
group.on('dragstart', () => {
|
||||
// Store original parent and position
|
||||
group.setAttr('originalParent', group.getParent());
|
||||
group.setAttr('originalPosition', group.position());
|
||||
|
||||
// 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 () => {
|
||||
group.opacity(1);
|
||||
await this.handleDeviceDrop(deviceData.id, group);
|
||||
});
|
||||
|
||||
// Right-click context menu
|
||||
group.on('contextmenu', (e) => {
|
||||
e.evt.preventDefault();
|
||||
this.showDeviceContextMenu(e, deviceData, group);
|
||||
});
|
||||
|
||||
devicesContainer.add(group);
|
||||
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) {
|
||||
try {
|
||||
await this.api.deleteDevice(deviceId);
|
||||
group.destroy();
|
||||
this.devices.delete(deviceId);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Notify table to sync
|
||||
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');
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
contextMenuList.addEventListener('click', handleAction);
|
||||
}
|
||||
|
||||
getNextDevicePosition(rackId) {
|
||||
// Find the lowest available slot (1-42)
|
||||
// U1 is at the bottom, so we fill from bottom to top
|
||||
const usedSlots = new Set();
|
||||
this.devices.forEach(device => {
|
||||
if (device.data.rack_id === rackId) {
|
||||
usedSlots.add(device.data.position);
|
||||
}
|
||||
});
|
||||
|
||||
// Find first available slot starting from U1 (bottom)
|
||||
for (let slot = 1; slot <= 42; slot++) {
|
||||
if (!usedSlots.has(slot)) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
// If all slots are full, return next slot (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) {
|
||||
const device = this.devices.get(deviceId);
|
||||
if (!device) return;
|
||||
|
||||
// Get device's center point for more accurate drop detection
|
||||
const absolutePos = deviceShape.getAbsolutePosition();
|
||||
const deviceCenterX = absolutePos.x + (this.deviceWidth / 2);
|
||||
const deviceCenterY = absolutePos.y + (this.deviceHeight / 2);
|
||||
|
||||
// Find which rack the device is over
|
||||
let targetRack = null;
|
||||
let targetRackId = null;
|
||||
|
||||
this.rackManager.racks.forEach((rack, rackId) => {
|
||||
const rackPos = rack.shape.getAbsolutePosition();
|
||||
const rackWidth = rack.data.width || this.rackManager.rackWidth;
|
||||
const rackHeight = rack.data.height || this.rackManager.rackHeight;
|
||||
|
||||
// Check if device center is within rack bounds
|
||||
if (deviceCenterX >= rackPos.x && deviceCenterX <= rackPos.x + rackWidth &&
|
||||
deviceCenterY >= rackPos.y && deviceCenterY <= rackPos.y + rackHeight) {
|
||||
targetRack = rack;
|
||||
targetRackId = rackId;
|
||||
}
|
||||
});
|
||||
|
||||
// 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 = device.data.rack_id;
|
||||
|
||||
// Calculate position within target rack
|
||||
const rackShape = targetRack.shape;
|
||||
const rackAbsolutePos = rackShape.getAbsolutePosition();
|
||||
const relativeY = absolutePos.y - rackAbsolutePos.y;
|
||||
|
||||
// Convert visual Y to slot position (1-42, where U1 is at bottom)
|
||||
const maxSlots = 42;
|
||||
const visualPosition = Math.round((relativeY - 10) / (this.deviceHeight + this.deviceSpacing));
|
||||
let newPosition = maxSlots - visualPosition; // Invert: bottom (high Y) = low slot number
|
||||
newPosition = Math.max(1, Math.min(42, newPosition)); // Clamp to 1-42
|
||||
|
||||
// Get devices in target rack and check for conflicts
|
||||
const devicesInTargetRack = Array.from(this.devices.values())
|
||||
.filter(d => d.data.rack_id === targetRackId && d.data.id !== deviceId)
|
||||
.sort((a, b) => a.data.position - b.data.position);
|
||||
|
||||
// Find available position
|
||||
let finalPosition = newPosition;
|
||||
const occupiedPositions = new Set(devicesInTargetRack.map(d => d.data.position));
|
||||
|
||||
while (occupiedPositions.has(finalPosition)) {
|
||||
finalPosition++;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Reposition device using helper method
|
||||
const rackUnits = device.data.rack_units || 1;
|
||||
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 });
|
||||
|
||||
// Compact positions in original rack if different
|
||||
if (originalRackId !== targetRackId) {
|
||||
this.compactRackDevices(originalRackId);
|
||||
}
|
||||
|
||||
this.layer.batchDraw();
|
||||
|
||||
// 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) {
|
||||
this.devices.forEach(device => {
|
||||
device.shape.draggable(draggable);
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (rect) {
|
||||
rect.width(this.deviceWidth);
|
||||
}
|
||||
if (text) {
|
||||
text.width(this.deviceWidth);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user