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 `
`; } }, { 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; } } }