First commit
This commit is contained in:
488
archive/old_public/js/rack-manager.js
Normal file
488
archive/old_public/js/rack-manager.js
Normal file
@@ -0,0 +1,488 @@
|
||||
export class RackManager {
|
||||
constructor(layer, api, deviceManager) {
|
||||
this.layer = layer;
|
||||
this.api = api;
|
||||
this.deviceManager = deviceManager;
|
||||
this.racks = new Map();
|
||||
this.rackPrefix = 'RACK';
|
||||
this.rackWidth = 520; // Fits 500px wide devices with margins
|
||||
this.rackHeight = 1510; // Fits 42 devices (42 * 35px + margins)
|
||||
this.rackSpacing = 80;
|
||||
this.gridSize = 600; // Default: rack width + spacing
|
||||
this.gridVertical = 1610; // Default: rack height + spacing (1510 + 100)
|
||||
this.racksLocked = true; // Start with racks locked
|
||||
this.nextX = 0; // Start at grid origin
|
||||
this.nextY = 0; // Start at grid origin
|
||||
// Note: loadSpacing() will be called after project ID is set
|
||||
}
|
||||
|
||||
loadSpacing() {
|
||||
const projectId = this.api.currentProjectId;
|
||||
const savedGridSize = localStorage.getItem(`gridSize_${projectId}`);
|
||||
const savedGridVertical = localStorage.getItem(`gridVertical_${projectId}`);
|
||||
|
||||
if (savedGridSize) {
|
||||
this.gridSize = parseInt(savedGridSize);
|
||||
this.rackSpacing = this.gridSize - this.rackWidth;
|
||||
} else {
|
||||
this.gridSize = 600; // Default: rack width + spacing
|
||||
}
|
||||
|
||||
if (savedGridVertical) {
|
||||
this.gridVertical = parseInt(savedGridVertical);
|
||||
} else {
|
||||
this.gridVertical = 1610; // Default: rack height + spacing (fits 42 devices)
|
||||
}
|
||||
}
|
||||
|
||||
saveSpacing() {
|
||||
const projectId = this.api.currentProjectId;
|
||||
localStorage.setItem(`gridSize_${projectId}`, this.gridSize.toString());
|
||||
localStorage.setItem(`gridVertical_${projectId}`, this.gridVertical.toString());
|
||||
}
|
||||
|
||||
async toggleRacksLock() {
|
||||
this.racksLocked = !this.racksLocked;
|
||||
this.racks.forEach(rack => {
|
||||
rack.shape.draggable(!this.racksLocked);
|
||||
});
|
||||
|
||||
// Update device draggability
|
||||
if (this.deviceManager) {
|
||||
this.deviceManager.updateDevicesDraggability(!this.racksLocked);
|
||||
}
|
||||
|
||||
// If locking, compact the grid (remove empty columns from the left)
|
||||
if (this.racksLocked) {
|
||||
await this.compactGrid();
|
||||
}
|
||||
|
||||
return this.racksLocked;
|
||||
}
|
||||
|
||||
async compactGrid() {
|
||||
if (this.racks.size === 0) return;
|
||||
|
||||
// Get all rack positions and calculate their grid coordinates
|
||||
const rackPositions = [];
|
||||
this.racks.forEach((rack, id) => {
|
||||
const gridX = Math.round(rack.data.x / this.gridSize);
|
||||
const gridY = Math.round(rack.data.y / this.gridVertical);
|
||||
rackPositions.push({ id, rack, gridX, gridY });
|
||||
});
|
||||
|
||||
// Find the minimum grid X (leftmost column that has racks)
|
||||
const minGridX = Math.min(...rackPositions.map(r => r.gridX));
|
||||
|
||||
// If minGridX is 0, grid is already compact
|
||||
if (minGridX === 0) return;
|
||||
|
||||
// Shift all racks left by minGridX columns
|
||||
const updatePromises = [];
|
||||
|
||||
for (const rackPos of rackPositions) {
|
||||
const newGridX = rackPos.gridX - minGridX;
|
||||
const newX = newGridX * this.gridSize;
|
||||
const newY = rackPos.gridY * this.gridVertical;
|
||||
|
||||
// Update visual position
|
||||
rackPos.rack.shape.position({ x: newX, y: newY });
|
||||
|
||||
// Update data
|
||||
rackPos.rack.data.x = newX;
|
||||
rackPos.rack.data.y = newY;
|
||||
|
||||
// Queue database update
|
||||
updatePromises.push(this.api.updateRackPosition(rackPos.id, newX, newY));
|
||||
}
|
||||
|
||||
// Redraw once
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Wait for all updates
|
||||
await Promise.all(updatePromises);
|
||||
}
|
||||
|
||||
snapToGrid(value, gridSize) {
|
||||
return Math.round(value / gridSize) * gridSize;
|
||||
}
|
||||
|
||||
async loadRacks() {
|
||||
try {
|
||||
const racks = await this.api.getRacks();
|
||||
racks.forEach(rackData => {
|
||||
this.createRackShape(rackData);
|
||||
});
|
||||
this.layer.batchDraw();
|
||||
} catch (err) {
|
||||
console.error('Failed to load racks:', err);
|
||||
}
|
||||
}
|
||||
|
||||
createRackShape(rackData) {
|
||||
const group = new Konva.Group({
|
||||
x: rackData.x,
|
||||
y: rackData.y,
|
||||
draggable: !this.racksLocked, // Locked by default
|
||||
id: `rack-${rackData.id}`
|
||||
});
|
||||
|
||||
// Rack background
|
||||
const rect = new Konva.Rect({
|
||||
width: rackData.width || this.rackWidth,
|
||||
height: rackData.height || this.rackHeight,
|
||||
fill: '#ffffff',
|
||||
stroke: '#333',
|
||||
strokeWidth: 2,
|
||||
shadowColor: 'black',
|
||||
shadowBlur: 5,
|
||||
shadowOpacity: 0.1,
|
||||
shadowOffset: { x: 2, y: 2 }
|
||||
});
|
||||
|
||||
// Rack name label (clickable)
|
||||
const nameLabel = new Konva.Text({
|
||||
x: 0,
|
||||
y: -30,
|
||||
width: rackData.width || this.rackWidth,
|
||||
text: rackData.name,
|
||||
fontSize: 16,
|
||||
fontStyle: 'bold',
|
||||
fill: '#333',
|
||||
align: 'center',
|
||||
name: 'rack-name'
|
||||
});
|
||||
|
||||
// Make name clickable
|
||||
nameLabel.on('click', () => {
|
||||
window.dispatchEvent(new CustomEvent('rename-rack', {
|
||||
detail: { rackId: rackData.id, rackData, rackShape: group }
|
||||
}));
|
||||
});
|
||||
|
||||
nameLabel.on('mouseenter', () => {
|
||||
document.body.style.cursor = 'pointer';
|
||||
nameLabel.fill('#4A90E2');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
nameLabel.on('mouseleave', () => {
|
||||
document.body.style.cursor = 'default';
|
||||
nameLabel.fill('#333');
|
||||
this.layer.batchDraw();
|
||||
});
|
||||
|
||||
// Container for devices
|
||||
const devicesLayer = new Konva.Group({
|
||||
name: 'devices-container'
|
||||
});
|
||||
|
||||
group.add(rect);
|
||||
group.add(nameLabel);
|
||||
group.add(devicesLayer);
|
||||
|
||||
// Grid snapping during drag
|
||||
group.on('dragmove', () => {
|
||||
const x = this.snapToGrid(group.x(), this.gridSize);
|
||||
const y = this.snapToGrid(group.y(), this.gridVertical);
|
||||
group.position({ x, y });
|
||||
});
|
||||
|
||||
// Drag end - update position in DB with smart positioning
|
||||
group.on('dragend', async () => {
|
||||
try {
|
||||
const newX = this.snapToGrid(group.x(), this.gridSize);
|
||||
const newY = this.snapToGrid(group.y(), this.gridVertical);
|
||||
|
||||
// Check if position is occupied by another rack
|
||||
await this.handleRackPlacement(rackData.id, newX, newY);
|
||||
} catch (err) {
|
||||
console.error('Failed to update rack position:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Right-click context menu
|
||||
group.on('contextmenu', (e) => {
|
||||
e.evt.preventDefault();
|
||||
this.showContextMenu(e, rackData, group);
|
||||
});
|
||||
|
||||
this.layer.add(group);
|
||||
this.racks.set(rackData.id, { data: rackData, shape: group });
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
async addRack() {
|
||||
try {
|
||||
const nextName = await this.api.getNextRackName(this.rackPrefix);
|
||||
|
||||
const rackData = await this.api.createRack(nextName, this.nextX, this.nextY);
|
||||
|
||||
this.createRackShape(rackData);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Update next position (using grid sizes)
|
||||
this.nextX += this.gridSize;
|
||||
if (this.nextX > 1200) {
|
||||
this.nextX = 0;
|
||||
this.nextY += this.gridVertical;
|
||||
}
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
return rackData;
|
||||
} catch (err) {
|
||||
console.error('Failed to add rack:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRack(rackId, group) {
|
||||
try {
|
||||
await this.api.deleteRack(rackId);
|
||||
group.destroy();
|
||||
this.racks.delete(rackId);
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Notify table to sync
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
} catch (err) {
|
||||
console.error('Failed to delete rack:', err);
|
||||
}
|
||||
}
|
||||
|
||||
showContextMenu(e, rackData, group) {
|
||||
const contextMenu = document.getElementById('contextMenu');
|
||||
const contextMenuList = document.getElementById('contextMenuList');
|
||||
|
||||
const lockText = this.racksLocked ? 'Unlock All Racks' : 'Lock All Racks';
|
||||
|
||||
// Build device types list with header
|
||||
let deviceTypesHTML = '<li class="menu-header">Add device:</li>';
|
||||
if (this.deviceManager && this.deviceManager.deviceTypes) {
|
||||
this.deviceManager.deviceTypes.forEach(type => {
|
||||
deviceTypesHTML += `<li data-action="add-device" data-device-type-id="${type.id}" data-device-type-name="${type.name}">${type.name}</li>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Build unlock/management options
|
||||
let managementHTML = `<li data-action="toggle-lock">${lockText}</li>`;
|
||||
|
||||
// Show delete and spacing controls only when unlocked
|
||||
if (!this.racksLocked) {
|
||||
const horizontalSpacing = this.gridSize - this.rackWidth;
|
||||
const verticalSpacing = this.gridVertical - this.rackHeight;
|
||||
|
||||
managementHTML += `
|
||||
<li data-action="delete">Delete Rack</li>
|
||||
<li class="divider"></li>
|
||||
<li class="spacing-control">
|
||||
<span class="spacing-label">Horizontal spacing: ${horizontalSpacing}px</span>
|
||||
<div class="spacing-buttons">
|
||||
<button class="spacing-btn" data-action="h-spacing-decrease">−</button>
|
||||
<button class="spacing-btn" data-action="h-spacing-increase">+</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="spacing-control">
|
||||
<span class="spacing-label">Vertical spacing: ${verticalSpacing}px</span>
|
||||
<div class="spacing-buttons">
|
||||
<button class="spacing-btn" data-action="v-spacing-decrease">−</button>
|
||||
<button class="spacing-btn" data-action="v-spacing-increase">+</button>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
contextMenuList.innerHTML = `
|
||||
${deviceTypesHTML}
|
||||
<li class="divider"></li>
|
||||
${managementHTML}
|
||||
`;
|
||||
|
||||
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;
|
||||
|
||||
// For spacing buttons, prevent default and stop propagation
|
||||
if (action && action.includes('spacing')) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
if (action === 'add-device') {
|
||||
const deviceTypeId = parseInt(evt.target.dataset.deviceTypeId);
|
||||
const deviceTypeName = evt.target.dataset.deviceTypeName;
|
||||
const deviceName = prompt(`Enter name for ${deviceTypeName}:`, deviceTypeName);
|
||||
|
||||
if (deviceName) {
|
||||
try {
|
||||
// Check if name will be auto-numbered
|
||||
const uniqueName = this.deviceManager.generateUniqueName(deviceName);
|
||||
if (uniqueName !== deviceName) {
|
||||
const proceed = confirm(`Device name "${deviceName}" is already in use.\n\nDevice will be named "${uniqueName}" instead.\n\nContinue?`);
|
||||
if (!proceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const position = this.deviceManager.getNextDevicePosition(rackData.id);
|
||||
await this.deviceManager.addDevice(deviceTypeId, rackData.id, position, deviceName);
|
||||
} catch (err) {
|
||||
alert('Failed to add device: ' + err.message);
|
||||
}
|
||||
}
|
||||
} else if (action === 'delete') {
|
||||
if (confirm(`Delete rack ${rackData.name}?`)) {
|
||||
this.deleteRack(rackData.id, group);
|
||||
}
|
||||
} else if (action === 'toggle-lock') {
|
||||
const isLocked = await this.toggleRacksLock();
|
||||
const statusText = isLocked ? 'Racks locked (grid compacted)' : 'Racks unlocked';
|
||||
// Close and reopen menu to refresh the lock state
|
||||
contextMenu.classList.add('hidden');
|
||||
setTimeout(() => {
|
||||
this.showContextMenu(e, rackData, group);
|
||||
}, 10);
|
||||
return; // Don't close menu handler
|
||||
} else if (action === 'h-spacing-increase') {
|
||||
await this.adjustSpacing('horizontal', 10);
|
||||
return; // Don't close menu
|
||||
} else if (action === 'h-spacing-decrease') {
|
||||
await this.adjustSpacing('horizontal', -10);
|
||||
return; // Don't close menu
|
||||
} else if (action === 'v-spacing-increase') {
|
||||
await this.adjustSpacing('vertical', 50);
|
||||
return; // Don't close menu
|
||||
} else if (action === 'v-spacing-decrease') {
|
||||
await this.adjustSpacing('vertical', -50);
|
||||
return; // Don't close menu
|
||||
}
|
||||
|
||||
contextMenu.classList.add('hidden');
|
||||
contextMenuList.removeEventListener('click', handleAction);
|
||||
};
|
||||
|
||||
contextMenuList.addEventListener('click', handleAction);
|
||||
}
|
||||
|
||||
getRackShape(rackId) {
|
||||
const rack = this.racks.get(rackId);
|
||||
return rack ? rack.shape : null;
|
||||
}
|
||||
|
||||
getRackData(rackId) {
|
||||
const rack = this.racks.get(rackId);
|
||||
return rack ? rack.data : null;
|
||||
}
|
||||
|
||||
async handleRackPlacement(movedRackId, newX, newY) {
|
||||
// Get all racks in the same row (same Y coordinate)
|
||||
const racksInRow = [];
|
||||
this.racks.forEach((rack, id) => {
|
||||
if (id !== movedRackId && rack.data.y === newY) {
|
||||
racksInRow.push({ id, rack, x: rack.data.x });
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by X position
|
||||
racksInRow.sort((a, b) => a.x - b.x);
|
||||
|
||||
// Check if new position is occupied
|
||||
const occupiedRack = racksInRow.find(r => r.x === newX);
|
||||
|
||||
if (occupiedRack) {
|
||||
// Position is occupied - shift all racks at and to the right of this position
|
||||
const racksToShift = racksInRow.filter(r => r.x >= newX);
|
||||
|
||||
// Shift each rack one grid position to the right
|
||||
for (const rackInfo of racksToShift) {
|
||||
const newRackX = rackInfo.x + this.gridSize;
|
||||
|
||||
// Update visual position
|
||||
rackInfo.rack.shape.position({ x: newRackX, y: newY });
|
||||
|
||||
// Update data
|
||||
rackInfo.rack.data.x = newRackX;
|
||||
rackInfo.rack.data.y = newY;
|
||||
|
||||
// Update in database
|
||||
await this.api.updateRackPosition(rackInfo.id, newRackX, newY);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the moved rack
|
||||
const movedRack = this.racks.get(movedRackId);
|
||||
if (movedRack) {
|
||||
movedRack.shape.position({ x: newX, y: newY });
|
||||
movedRack.data.x = newX;
|
||||
movedRack.data.y = newY;
|
||||
await this.api.updateRackPosition(movedRackId, newX, newY);
|
||||
}
|
||||
|
||||
// Redraw
|
||||
this.layer.batchDraw();
|
||||
}
|
||||
|
||||
async adjustSpacing(direction, delta) {
|
||||
// Calculate grid coordinates for all racks BEFORE changing spacing
|
||||
const rackGridPositions = new Map();
|
||||
|
||||
this.racks.forEach((rack, id) => {
|
||||
const gridX = Math.round(rack.data.x / this.gridSize);
|
||||
const gridY = Math.round(rack.data.y / this.gridVertical);
|
||||
rackGridPositions.set(id, { gridX, gridY });
|
||||
});
|
||||
|
||||
// Adjust spacing (this updates the grid references)
|
||||
if (direction === 'horizontal') {
|
||||
const newSpacing = (this.gridSize - this.rackWidth) + delta;
|
||||
if (newSpacing < 10) return; // Minimum spacing
|
||||
this.gridSize = this.rackWidth + newSpacing;
|
||||
this.rackSpacing = newSpacing; // Update the spacing value
|
||||
} else {
|
||||
const newSpacing = (this.gridVertical - this.rackHeight) + delta;
|
||||
if (newSpacing < 10) return; // Minimum spacing
|
||||
this.gridVertical = this.rackHeight + newSpacing;
|
||||
}
|
||||
|
||||
// Batch all position updates
|
||||
const updatePromises = [];
|
||||
|
||||
// Recalculate all rack positions at once
|
||||
for (const [id, gridPos] of rackGridPositions) {
|
||||
const rack = this.racks.get(id);
|
||||
if (!rack) continue;
|
||||
|
||||
const newX = gridPos.gridX * this.gridSize;
|
||||
const newY = gridPos.gridY * this.gridVertical;
|
||||
|
||||
// Update visual position
|
||||
rack.shape.position({ x: newX, y: newY });
|
||||
|
||||
// Update data
|
||||
rack.data.x = newX;
|
||||
rack.data.y = newY;
|
||||
|
||||
// Queue database update (don't await yet)
|
||||
updatePromises.push(this.api.updateRackPosition(id, newX, newY));
|
||||
}
|
||||
|
||||
// Redraw once for all changes
|
||||
this.layer.batchDraw();
|
||||
|
||||
// Wait for all database updates to complete
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// Save spacing to localStorage
|
||||
this.saveSpacing();
|
||||
|
||||
// Update status
|
||||
const horizontalSpacing = this.gridSize - this.rackWidth;
|
||||
const verticalSpacing = this.gridVertical - this.rackHeight;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user