First commit
This commit is contained in:
805
public/js/managers/table-manager.js
Normal file
805
public/js/managers/table-manager.js
Normal file
@@ -0,0 +1,805 @@
|
||||
export class TableManager {
|
||||
constructor(api, rackManager, deviceManager, connectionManager) {
|
||||
this.api = api;
|
||||
this.rackManager = rackManager;
|
||||
this.deviceManager = deviceManager;
|
||||
this.connectionManager = connectionManager;
|
||||
|
||||
this.currentTable = null; // 'racks', 'devices', 'connections'
|
||||
this.gridApi = null;
|
||||
this.gridColumnApi = null;
|
||||
this.tableContainer = document.getElementById('tableContent');
|
||||
}
|
||||
|
||||
isTableVisible() {
|
||||
return this.currentTable !== null;
|
||||
}
|
||||
|
||||
getCurrentTableType() {
|
||||
return this.currentTable;
|
||||
}
|
||||
|
||||
// Show specific table view
|
||||
async showTable(tableType) {
|
||||
// tableType can be: 'racks-table', 'devices-table', 'connections-table'
|
||||
const tableMap = {
|
||||
'racks-table': 'racks',
|
||||
'devices-table': 'devices',
|
||||
'connections-table': 'connections'
|
||||
};
|
||||
|
||||
this.currentTable = tableMap[tableType];
|
||||
|
||||
// Clear existing grid
|
||||
if (this.gridApi) {
|
||||
this.gridApi.destroy();
|
||||
this.gridApi = null;
|
||||
}
|
||||
|
||||
// Clear container to ensure no stale DOM elements
|
||||
this.tableContainer.innerHTML = '';
|
||||
|
||||
// Render appropriate table
|
||||
switch (this.currentTable) {
|
||||
case 'racks':
|
||||
await this.showRacksTable();
|
||||
break;
|
||||
case 'devices':
|
||||
await this.showDevicesTable();
|
||||
break;
|
||||
case 'connections':
|
||||
await this.showConnectionsTable();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
hideTable() {
|
||||
if (this.gridApi) {
|
||||
this.gridApi.destroy();
|
||||
this.gridApi = null;
|
||||
}
|
||||
this.currentTable = null;
|
||||
this.tableContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// ===== RACKS TABLE =====
|
||||
async showRacksTable() {
|
||||
const racks = await this.api.getRacks();
|
||||
|
||||
// Sort alphabetically by name
|
||||
const sortedRacks = racks.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const columnDefs = [
|
||||
{
|
||||
headerName: 'Rack Name',
|
||||
field: 'name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true
|
||||
},
|
||||
{
|
||||
headerName: 'Position X',
|
||||
field: 'x',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${Math.round(params.value)}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Position Y',
|
||||
field: 'y',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${Math.round(params.value)}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Width',
|
||||
field: 'width',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${params.value}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Height',
|
||||
field: 'height',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueFormatter: params => `${params.value}px`
|
||||
},
|
||||
{
|
||||
headerName: 'Device Count',
|
||||
field: 'deviceCount',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueGetter: params => {
|
||||
// Count devices in this rack
|
||||
const devices = this.deviceManager.getAllDevices();
|
||||
return devices.filter(d => d.rack_id === params.data.id).length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
columnDefs: columnDefs,
|
||||
rowData: sortedRacks,
|
||||
rowSelection: 'multiple',
|
||||
animateRows: true,
|
||||
enableCellTextSelection: true,
|
||||
defaultColDef: {
|
||||
flex: 1,
|
||||
minWidth: 100,
|
||||
resizable: true
|
||||
},
|
||||
onCellValueChanged: (params) => this.onRackCellValueChanged(params),
|
||||
onSelectionChanged: () => this.updateToolbarButtons(),
|
||||
overlayNoRowsTemplate: '<span style="padding: 10px; color: #999;">No racks found</span>'
|
||||
};
|
||||
|
||||
this.gridApi = agGrid.createGrid(this.tableContainer, gridOptions);
|
||||
}
|
||||
|
||||
async onRackCellValueChanged(params) {
|
||||
const rackId = params.data.id;
|
||||
const field = params.colDef.field;
|
||||
const newValue = params.newValue;
|
||||
|
||||
try {
|
||||
if (field === 'name') {
|
||||
await this.api.updateRackName(rackId, newValue);
|
||||
|
||||
// Update canvas
|
||||
const rackShape = this.rackManager.getRackShape(rackId);
|
||||
if (rackShape) {
|
||||
const nameLabel = rackShape.findOne('.rack-name');
|
||||
if (nameLabel) {
|
||||
nameLabel.text(newValue);
|
||||
this.rackManager.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Update local data
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
if (rackData) {
|
||||
rackData.name = newValue;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update rack:', err);
|
||||
alert('Failed to update rack: ' + err.message);
|
||||
// Revert the change
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DEVICES TABLE =====
|
||||
async showDevicesTable() {
|
||||
const devices = await this.api.getDevices();
|
||||
const racks = await this.api.getRacks();
|
||||
const deviceTypes = await this.api.getDeviceTypes();
|
||||
|
||||
const columnDefs = [
|
||||
{
|
||||
headerName: 'Device Name',
|
||||
field: 'name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true
|
||||
},
|
||||
{
|
||||
headerName: 'Type',
|
||||
field: 'type_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: deviceTypes.map(t => t.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Rack',
|
||||
field: 'rack_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: racks.map(r => r.name)
|
||||
},
|
||||
valueGetter: params => {
|
||||
const rack = racks.find(r => r.id === params.data.rack_id);
|
||||
return rack ? rack.name : '';
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Slot/Position',
|
||||
field: 'position',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `U${params.value}`,
|
||||
cellEditor: 'agNumberCellEditor',
|
||||
cellEditorParams: {
|
||||
min: 1,
|
||||
max: 42,
|
||||
precision: 0
|
||||
},
|
||||
valueSetter: params => {
|
||||
const newValue = parseInt(params.newValue);
|
||||
if (newValue >= 1 && newValue <= 42) {
|
||||
params.data.position = newValue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Form Factor',
|
||||
field: 'rack_units',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `${params.value || 1}U`,
|
||||
cellEditor: 'agNumberCellEditor',
|
||||
cellEditorParams: {
|
||||
min: 1,
|
||||
max: 42,
|
||||
precision: 0
|
||||
},
|
||||
valueSetter: params => {
|
||||
const newValue = parseInt(params.newValue);
|
||||
if (newValue >= 1 && newValue <= 42) {
|
||||
params.data.rack_units = newValue;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Ports',
|
||||
field: 'ports_count',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter'
|
||||
},
|
||||
{
|
||||
headerName: 'Color',
|
||||
field: 'color',
|
||||
editable: false,
|
||||
sortable: false,
|
||||
cellRenderer: params => {
|
||||
return `<div style="width: 100%; height: 100%; background-color: ${params.value}; border-radius: 3px;"></div>`;
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Connections',
|
||||
field: 'connectionCount',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueGetter: params => {
|
||||
// Count connections for this device
|
||||
const connections = Array.from(this.connectionManager.connections.values());
|
||||
return connections.filter(c =>
|
||||
c.data.source_device_id === params.data.id ||
|
||||
c.data.target_device_id === params.data.id
|
||||
).length;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
columnDefs: columnDefs,
|
||||
rowData: devices,
|
||||
rowSelection: 'multiple',
|
||||
animateRows: true,
|
||||
enableCellTextSelection: true,
|
||||
defaultColDef: {
|
||||
flex: 1,
|
||||
minWidth: 100,
|
||||
resizable: true
|
||||
},
|
||||
onCellValueChanged: (params) => this.onDeviceCellValueChanged(params, racks, deviceTypes),
|
||||
onSelectionChanged: () => this.updateToolbarButtons(),
|
||||
overlayNoRowsTemplate: '<span style="padding: 10px; color: #999;">No devices found</span>'
|
||||
};
|
||||
|
||||
this.gridApi = agGrid.createGrid(this.tableContainer, gridOptions);
|
||||
}
|
||||
|
||||
async onDeviceCellValueChanged(params, racks, deviceTypes) {
|
||||
const deviceId = params.data.id;
|
||||
const field = params.colDef.field;
|
||||
const newValue = params.newValue;
|
||||
|
||||
try {
|
||||
if (field === 'name') {
|
||||
// Check if name is already taken
|
||||
if (this.deviceManager.isDeviceNameTaken(newValue, deviceId)) {
|
||||
alert(`Device name "${newValue}" is already in use. Please choose a different name.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.updateDeviceName(deviceId, newValue);
|
||||
|
||||
// Update canvas
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
if (deviceShape) {
|
||||
const nameLabel = deviceShape.findOne('.device-text');
|
||||
if (nameLabel) {
|
||||
nameLabel.text(newValue);
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Update local data
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceData) {
|
||||
deviceData.name = newValue;
|
||||
}
|
||||
} else if (field === 'rack_name') {
|
||||
// Find the rack by name
|
||||
const rack = racks.find(r => r.name === newValue);
|
||||
if (rack) {
|
||||
const newPosition = this.deviceManager.getNextDevicePosition(rack.id);
|
||||
await this.api.request(`/api/devices/${deviceId}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: rack.id, position: newPosition })
|
||||
});
|
||||
|
||||
// Update device on canvas
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceShape && deviceData) {
|
||||
const oldRackId = deviceData.rack_id;
|
||||
deviceData.rack_id = rack.id;
|
||||
deviceData.position = newPosition;
|
||||
|
||||
// Move to new rack's container
|
||||
const newRackShape = this.rackManager.getRackShape(rack.id);
|
||||
if (newRackShape) {
|
||||
const newDevicesContainer = newRackShape.findOne('.devices-container');
|
||||
deviceShape.moveTo(newDevicesContainer);
|
||||
|
||||
// Calculate visual position using helper method
|
||||
const rackUnits = deviceData.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(rack.id);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.deviceManager.calculateDeviceY(newPosition, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
|
||||
// Compact old rack
|
||||
if (oldRackId !== rack.id) {
|
||||
this.deviceManager.compactRackDevices(oldRackId);
|
||||
}
|
||||
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh table to show updated position
|
||||
this.refreshTable();
|
||||
}
|
||||
} else if (field === 'position') {
|
||||
const rackId = params.data.rack_id;
|
||||
const newSlot = parseInt(newValue);
|
||||
const rackUnits = params.data.rack_units || 1;
|
||||
|
||||
// Validate slot range (1-42)
|
||||
if (newSlot < 1 || newSlot > 42) {
|
||||
alert('Slot position must be between U1 and U42');
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate that device with its rack_units fits in the rack
|
||||
if (newSlot + rackUnits - 1 > 42) {
|
||||
alert(`Device with ${rackUnits}U form factor cannot fit at position U${newSlot}. Maximum position is U${43 - rackUnits}.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for slot conflicts with other devices
|
||||
const conflict = this.deviceManager.checkSlotConflict(rackId, newSlot, rackUnits, deviceId);
|
||||
if (conflict) {
|
||||
alert(`Slot conflict detected: ${conflict}`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.request(`/api/devices/${deviceId}/rack`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackId: rackId, position: newSlot })
|
||||
});
|
||||
|
||||
// Update device position on canvas using helper method
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceShape && deviceData) {
|
||||
deviceData.position = newSlot;
|
||||
const rackUnits = deviceData.rack_units || 1;
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.deviceManager.calculateDeviceY(newSlot, rackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
} else if (field === 'rack_units') {
|
||||
const rackId = params.data.rack_id;
|
||||
const position = params.data.position;
|
||||
const newRackUnits = parseInt(newValue);
|
||||
|
||||
// Validate that device with its new rack_units fits in the rack
|
||||
if (position + newRackUnits - 1 > 42) {
|
||||
alert(`Device with ${newRackUnits}U form factor cannot fit at position U${position}. Maximum form factor at this position is ${43 - position}U.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for slot conflicts with other devices
|
||||
const conflict = this.deviceManager.checkSlotConflict(rackId, position, newRackUnits, deviceId);
|
||||
if (conflict) {
|
||||
alert(`Slot conflict detected: ${conflict}`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.api.request(`/api/devices/${deviceId}/rack-units`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ rackUnits: newRackUnits })
|
||||
});
|
||||
|
||||
// Update device rendering on canvas
|
||||
const deviceShape = this.deviceManager.getDeviceShape(deviceId);
|
||||
const deviceData = this.deviceManager.getDeviceData(deviceId);
|
||||
if (deviceShape && deviceData) {
|
||||
deviceData.rack_units = newRackUnits;
|
||||
|
||||
// Update device height
|
||||
const newHeight = (this.deviceManager.deviceHeight * newRackUnits) + (this.deviceManager.deviceSpacing * (newRackUnits - 1));
|
||||
const rect = deviceShape.findOne('Rect');
|
||||
const text = deviceShape.findOne('.device-text');
|
||||
if (rect) {
|
||||
rect.height(newHeight);
|
||||
}
|
||||
if (text) {
|
||||
text.height(newHeight);
|
||||
}
|
||||
|
||||
// Reposition device since height changed using helper method
|
||||
const rackData = this.rackManager.getRackData(rackId);
|
||||
const rackHeight = rackData?.height || this.rackManager.rackHeight;
|
||||
const newY = this.deviceManager.calculateDeviceY(position, newRackUnits, rackHeight);
|
||||
deviceShape.position({ x: 10, y: newY });
|
||||
|
||||
this.deviceManager.layer.batchDraw();
|
||||
}
|
||||
|
||||
// Notify canvas that data changed
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
} else if (field === 'type_name') {
|
||||
// Find device type by name
|
||||
const deviceType = deviceTypes.find(dt => dt.name === newValue);
|
||||
if (deviceType) {
|
||||
// Note: We would need an API endpoint to update device type
|
||||
// For now, just show a message
|
||||
alert('Changing device type requires updating the device_type_id in the database. This feature needs backend support.');
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update device:', err);
|
||||
alert('Failed to update device: ' + err.message);
|
||||
// Revert the change
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CONNECTIONS TABLE =====
|
||||
async showConnectionsTable() {
|
||||
const connections = await this.api.getConnections();
|
||||
const devices = await this.api.getDevices();
|
||||
|
||||
// Enrich connection data with device names
|
||||
const enrichedConnections = connections.map(conn => {
|
||||
const sourceDevice = devices.find(d => d.id === conn.source_device_id);
|
||||
const targetDevice = devices.find(d => d.id === conn.target_device_id);
|
||||
|
||||
return {
|
||||
...conn,
|
||||
source_device_name: sourceDevice ? sourceDevice.name : 'Unknown',
|
||||
target_device_name: targetDevice ? targetDevice.name : 'Unknown',
|
||||
source_device_type: sourceDevice ? sourceDevice.type_name : '',
|
||||
target_device_type: targetDevice ? targetDevice.type_name : ''
|
||||
};
|
||||
});
|
||||
|
||||
const columnDefs = [
|
||||
{
|
||||
headerName: 'Source Device',
|
||||
field: 'source_device_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: devices.map(d => d.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Source Port',
|
||||
field: 'source_port',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `Port ${params.value}`
|
||||
},
|
||||
{
|
||||
headerName: 'Dest Device',
|
||||
field: 'target_device_name',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
cellEditor: 'agSelectCellEditor',
|
||||
cellEditorParams: {
|
||||
values: devices.map(d => d.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
headerName: 'Dest Port',
|
||||
field: 'target_port',
|
||||
editable: true,
|
||||
sortable: true,
|
||||
filter: 'agNumberColumnFilter',
|
||||
valueFormatter: params => `Port ${params.value}`
|
||||
},
|
||||
{
|
||||
headerName: 'Status',
|
||||
field: 'status',
|
||||
editable: false,
|
||||
sortable: true,
|
||||
valueGetter: params => {
|
||||
// Validate connection
|
||||
const sourceDevice = devices.find(d => d.id === params.data.source_device_id);
|
||||
const targetDevice = devices.find(d => d.id === params.data.target_device_id);
|
||||
|
||||
if (!sourceDevice || !targetDevice) return 'Invalid';
|
||||
if (params.data.source_port >= sourceDevice.ports_count) return 'Invalid Port';
|
||||
if (params.data.target_port >= targetDevice.ports_count) return 'Invalid Port';
|
||||
|
||||
return 'Valid';
|
||||
},
|
||||
cellStyle: params => {
|
||||
if (params.value === 'Valid') {
|
||||
return { color: '#4CAF50', fontWeight: 'bold' };
|
||||
} else {
|
||||
return { color: '#d32f2f', fontWeight: 'bold' };
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const gridOptions = {
|
||||
columnDefs: columnDefs,
|
||||
rowData: enrichedConnections,
|
||||
rowSelection: 'multiple',
|
||||
animateRows: true,
|
||||
enableCellTextSelection: true,
|
||||
defaultColDef: {
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
resizable: true
|
||||
},
|
||||
onCellValueChanged: (params) => this.onConnectionCellValueChanged(params, devices),
|
||||
onSelectionChanged: () => this.updateToolbarButtons(),
|
||||
overlayNoRowsTemplate: '<span style="padding: 10px; color: #999;">No connections found</span>'
|
||||
};
|
||||
|
||||
this.gridApi = agGrid.createGrid(this.tableContainer, gridOptions);
|
||||
}
|
||||
|
||||
async onConnectionCellValueChanged(params, devices) {
|
||||
const connectionId = params.data.id;
|
||||
const field = params.colDef.field;
|
||||
const newValue = params.newValue;
|
||||
|
||||
try {
|
||||
let sourceDeviceId = params.data.source_device_id;
|
||||
let sourcePort = params.data.source_port;
|
||||
let targetDeviceId = params.data.target_device_id;
|
||||
let targetPort = params.data.target_port;
|
||||
|
||||
// Update the field that was changed
|
||||
if (field === 'source_device_name') {
|
||||
const device = devices.find(d => d.name === newValue);
|
||||
if (!device) {
|
||||
alert(`Device "${newValue}" not found.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
sourceDeviceId = device.id;
|
||||
params.data.source_device_id = device.id;
|
||||
params.data.source_device_type = device.type_name;
|
||||
} else if (field === 'source_port') {
|
||||
sourcePort = parseInt(newValue);
|
||||
const sourceDevice = devices.find(d => d.id === sourceDeviceId);
|
||||
if (sourcePort < 0 || sourcePort >= sourceDevice.ports_count) {
|
||||
alert(`Invalid source port. Device "${sourceDevice.name}" has ports 0-${sourceDevice.ports_count - 1}.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if port is already in use by another connection
|
||||
const connections = await this.api.getConnections();
|
||||
const portInUse = connections.some(c =>
|
||||
c.id !== connectionId &&
|
||||
((c.source_device_id === sourceDeviceId && c.source_port === sourcePort) ||
|
||||
(c.target_device_id === sourceDeviceId && c.target_port === sourcePort))
|
||||
);
|
||||
if (portInUse) {
|
||||
alert(`Port ${sourcePort} is already in use on device "${sourceDevice.name}".`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
} else if (field === 'target_device_name') {
|
||||
const device = devices.find(d => d.name === newValue);
|
||||
if (!device) {
|
||||
alert(`Device "${newValue}" not found.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
targetDeviceId = device.id;
|
||||
params.data.target_device_id = device.id;
|
||||
params.data.target_device_type = device.type_name;
|
||||
} else if (field === 'target_port') {
|
||||
targetPort = parseInt(newValue);
|
||||
const targetDevice = devices.find(d => d.id === targetDeviceId);
|
||||
if (targetPort < 0 || targetPort >= targetDevice.ports_count) {
|
||||
alert(`Invalid target port. Device "${targetDevice.name}" has ports 0-${targetDevice.ports_count - 1}.`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if port is already in use by another connection
|
||||
const connections = await this.api.getConnections();
|
||||
const portInUse = connections.some(c =>
|
||||
c.id !== connectionId &&
|
||||
((c.source_device_id === targetDeviceId && c.source_port === targetPort) ||
|
||||
(c.target_device_id === targetDeviceId && c.target_port === targetPort))
|
||||
);
|
||||
if (portInUse) {
|
||||
alert(`Port ${targetPort} is already in use on device "${targetDevice.name}".`);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update connection in database
|
||||
await this.api.request(`/api/connections/${connectionId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
sourceDeviceId,
|
||||
sourcePort,
|
||||
targetDeviceId,
|
||||
targetPort
|
||||
})
|
||||
});
|
||||
|
||||
// Update canvas - delete and recreate the connection
|
||||
await this.connectionManager.deleteConnection(connectionId);
|
||||
const newConnection = await this.api.getConnections();
|
||||
const updatedConnection = newConnection.find(c => c.id === connectionId);
|
||||
if (updatedConnection) {
|
||||
this.connectionManager.createConnectionShape(updatedConnection);
|
||||
this.connectionManager.layer.batchDraw();
|
||||
}
|
||||
|
||||
// Refresh table to show updated data
|
||||
this.refreshTable();
|
||||
} catch (err) {
|
||||
console.error('Failed to update connection:', err);
|
||||
alert('Failed to update connection: ' + err.message);
|
||||
params.data[field] = params.oldValue;
|
||||
this.gridApi.refreshCells();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== REFRESH & SYNC =====
|
||||
async refreshTable() {
|
||||
if (!this.currentTable) return;
|
||||
|
||||
const tableType = `${this.currentTable}-table`;
|
||||
await this.showTable(tableType);
|
||||
}
|
||||
|
||||
async syncFromCanvas() {
|
||||
// Called when canvas data changes - refresh the table
|
||||
if (this.isTableVisible()) {
|
||||
await this.refreshTable();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CRUD OPERATIONS =====
|
||||
async addRow() {
|
||||
try {
|
||||
if (this.currentTable === 'racks') {
|
||||
await this.rackManager.addRack();
|
||||
await this.refreshTable();
|
||||
} else if (this.currentTable === 'devices') {
|
||||
alert('To add a device, please use the canvas view (right-click on a rack).');
|
||||
} else if (this.currentTable === 'connections') {
|
||||
alert('To add a connection, please use the canvas view (right-click on a device).');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add row:', err);
|
||||
alert('Failed to add row: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSelectedRows() {
|
||||
const selectedRows = this.gridApi.getSelectedRows();
|
||||
|
||||
if (selectedRows.length === 0) {
|
||||
alert('Please select rows to delete.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Delete ${selectedRows.length} row(s)?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete all rows with suppressed events to avoid race conditions
|
||||
for (const row of selectedRows) {
|
||||
if (this.currentTable === 'racks') {
|
||||
const rackShape = this.rackManager.getRackShape(row.id);
|
||||
await this.rackManager.deleteRack(row.id, rackShape, true); // suppress event
|
||||
} else if (this.currentTable === 'devices') {
|
||||
const deviceShape = this.deviceManager.getDeviceShape(row.id);
|
||||
await this.deviceManager.deleteDevice(row.id, deviceShape, true); // suppress event
|
||||
} else if (this.currentTable === 'connections') {
|
||||
const conn = this.connectionManager.connections.get(row.id);
|
||||
const line = conn ? conn.shape : null;
|
||||
const handles = conn ? conn.handles : null;
|
||||
await this.connectionManager.deleteConnection(row.id, line, handles, true); // suppress event
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch single event after all deletions complete
|
||||
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
|
||||
|
||||
await this.refreshTable();
|
||||
} catch (err) {
|
||||
console.error('Failed to delete rows:', err);
|
||||
alert('Failed to delete rows: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
updateToolbarButtons() {
|
||||
const deleteBtn = document.getElementById('deleteTableRowBtn');
|
||||
if (deleteBtn && this.gridApi) {
|
||||
const selectedRows = this.gridApi.getSelectedRows();
|
||||
deleteBtn.disabled = selectedRows.length === 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user