793 lines
26 KiB
JavaScript
793 lines
26 KiB
JavaScript
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;
|
|
}
|
|
|
|
// 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()
|
|
};
|
|
|
|
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()
|
|
};
|
|
|
|
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()
|
|
};
|
|
|
|
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 {
|
|
for (const row of selectedRows) {
|
|
if (this.currentTable === 'racks') {
|
|
const rackShape = this.rackManager.getRackShape(row.id);
|
|
await this.rackManager.deleteRack(row.id, rackShape);
|
|
} else if (this.currentTable === 'devices') {
|
|
const deviceShape = this.deviceManager.getDeviceShape(row.id);
|
|
await this.deviceManager.deleteDevice(row.id, deviceShape);
|
|
} else if (this.currentTable === 'connections') {
|
|
await this.connectionManager.deleteConnection(row.id);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|