Files
datacenter-designer/archive/old_public/js/connection-manager.js
Stefano Manfredi 3431a121a9 First commit
2025-10-27 11:57:38 +00:00

902 lines
29 KiB
JavaScript

export class ConnectionManager {
constructor(layer, api, deviceManager, rackManager) {
this.layer = layer;
this.api = api;
this.deviceManager = deviceManager;
this.rackManager = rackManager;
this.connections = new Map();
this.connectionLayer = new Konva.Layer();
this.pendingConnection = null;
this.tempLine = null;
this.currentView = 'physical'; // Track current view
this.selectedConnection = null; // Track selected connection for keyboard deletion
// Set up keyboard event listener for Delete key
this.setupKeyboardListeners();
}
setupKeyboardListeners() {
document.addEventListener('keydown', (e) => {
if (e.key === 'Delete' || e.key === 'Backspace') {
if (this.selectedConnection) {
e.preventDefault();
const conn = this.connections.get(this.selectedConnection);
if (conn) {
if (confirm('Delete this connection?')) {
this.deleteConnection(this.selectedConnection, conn.shape, conn.handles);
}
}
}
} else if (e.key === 'Escape') {
// Deselect connection on Escape
this.deselectConnection();
}
});
}
selectConnection(connectionId, line, handles) {
// Deselect previous connection
this.deselectConnection();
// Select this connection
this.selectedConnection = connectionId;
line.stroke('#FF6B6B'); // Red highlight for selected
line.strokeWidth(3);
handles.forEach(h => h.opacity(1));
this.connectionLayer.batchDraw();
}
deselectConnection() {
if (this.selectedConnection) {
const conn = this.connections.get(this.selectedConnection);
if (conn) {
conn.shape.stroke('#000000');
conn.shape.strokeWidth(1);
conn.handles.forEach(h => h.opacity(0));
this.connectionLayer.batchDraw();
}
this.selectedConnection = null;
}
}
getConnectionLayer() {
return this.connectionLayer;
}
setCurrentView(viewType) {
this.currentView = viewType;
// Reload all connections to use waypoints for the new view
this.reloadConnectionsForView();
}
async reloadConnectionsForView() {
// Clear existing connections from layer
this.connections.forEach(conn => {
conn.shape.destroy();
conn.handles.forEach(h => h.destroy());
});
this.connections.clear();
// Reload connections with view-specific waypoints
try {
const connections = await this.api.getConnections();
connections.forEach(connData => {
this.createConnectionLine(connData);
});
this.connectionLayer.batchDraw();
} catch (err) {
console.error('Failed to reload connections:', err);
}
}
isConnectionMode() {
return this.pendingConnection !== null;
}
async loadConnections() {
try {
const connections = await this.api.getConnections();
connections.forEach(connData => {
this.createConnectionLine(connData);
});
this.connectionLayer.batchDraw();
} catch (err) {
console.error('Failed to load connections:', err);
}
}
createConnectionLine(connData) {
const sourceDevice = this.deviceManager.getDeviceShape(connData.source_device_id);
const targetDevice = this.deviceManager.getDeviceShape(connData.target_device_id);
if (!sourceDevice || !targetDevice) {
console.error('Device shapes not found for connection:', connData);
return;
}
const conn = this.drawConnection(sourceDevice, targetDevice, connData);
this.connections.set(connData.id, conn);
return conn;
}
// Alias for table-manager compatibility
createConnectionShape(connData) {
return this.createConnectionLine(connData);
}
drawConnection(sourceShape, targetShape, connData) {
const deviceWidth = this.deviceManager.deviceWidth;
const deviceHeight = this.deviceManager.deviceHeight;
const sourcePos = this.getDeviceAbsolutePosition(sourceShape);
const targetPos = this.getDeviceAbsolutePosition(targetShape);
// Calculate edge points based on relative positions
const { sourcePoint, targetPoint } = this.getEdgeConnectionPoints(
sourcePos, targetPos, deviceWidth, deviceHeight
);
// Load waypoints based on current view
let points;
let viewWaypoints = null;
// Get view-specific waypoints
if (this.currentView === 'physical' && connData.waypoints_physical) {
viewWaypoints = typeof connData.waypoints_physical === 'string'
? JSON.parse(connData.waypoints_physical)
: connData.waypoints_physical;
} else if (this.currentView === 'logical' && connData.waypoints_logical) {
viewWaypoints = typeof connData.waypoints_logical === 'string'
? JSON.parse(connData.waypoints_logical)
: connData.waypoints_logical;
} else if (connData.waypoints) {
// Fallback to legacy waypoints column
viewWaypoints = typeof connData.waypoints === 'string'
? JSON.parse(connData.waypoints)
: connData.waypoints;
}
if (viewWaypoints && viewWaypoints.length > 0) {
// Use saved waypoints for this view
const hasEdgePoints = viewWaypoints[0].isEdge !== undefined;
if (hasEdgePoints) {
// Use saved edge points and middle waypoints
const edgeStart = viewWaypoints.find((p, i) => p.isEdge && i === 0) || { x: sourcePoint.x, y: sourcePoint.y };
const edgeEnd = viewWaypoints.find((p, i) => p.isEdge && i === viewWaypoints.length - 1) || { x: targetPoint.x, y: targetPoint.y };
const middleWaypoints = viewWaypoints.filter(p => !p.isEdge);
points = this.rebuildOrthogonalPath(edgeStart, middleWaypoints, edgeEnd);
} else {
// Old format: saved waypoints are only middle points
points = this.rebuildOrthogonalPath(sourcePoint, viewWaypoints, targetPoint);
}
} else {
// No waypoints for this view - create default path
points = this.createSimpleOrthogonalPath(sourcePoint, targetPoint);
}
// Create the line
const line = new Konva.Line({
points: points,
stroke: '#000000',
strokeWidth: 1,
lineCap: 'round',
lineJoin: 'round',
id: `connection-${connData.id}`,
opacity: 0.8,
hitStrokeWidth: 10
});
// Add line to layer FIRST so it's drawn underneath handles
this.connectionLayer.add(line);
// Create draggable handles for ALL points (including start and end)
const handles = [];
const numPoints = points.length / 2;
for (let i = 0; i < numPoints; i++) {
const handle = new Konva.Circle({
x: points[i * 2],
y: points[i * 2 + 1],
radius: 4,
fill: '#4A90E2',
stroke: '#fff',
strokeWidth: 1,
draggable: true,
opacity: 0, // Hidden by default
name: `handle-${i}`
});
handles.push(handle);
// Add handle AFTER line, so handles are on top
this.connectionLayer.add(handle);
}
// Click to select connection
line.on('click', (e) => {
e.cancelBubble = true;
this.selectConnection(connData.id, line, handles);
});
// Hover behavior: show handles and highlight line
line.on('mouseenter', () => {
if (this.selectedConnection !== connData.id) {
line.stroke('#4A90E2'); // Blue highlight
line.strokeWidth(2);
handles.forEach(h => h.opacity(1));
this.connectionLayer.batchDraw();
}
});
line.on('mouseleave', () => {
// Only hide if no handle is being dragged and not selected
const isDragging = handles.some(h => h.isDragging());
if (!isDragging && this.selectedConnection !== connData.id) {
line.stroke('#000000');
line.strokeWidth(1);
handles.forEach(h => h.opacity(0));
this.connectionLayer.batchDraw();
}
});
// Simplified handle event listeners
handles.forEach((handle, idx) => {
const isEdgeHandle = (idx === 0 || idx === handles.length - 1);
handle.on('mouseenter', () => {
// Keep line highlighted and handles visible when hovering over handles
line.stroke('#4A90E2');
line.strokeWidth(2);
handles.forEach(h => h.opacity(1));
this.connectionLayer.batchDraw();
});
handle.on('mousedown', (e) => e.cancelBubble = true);
handle.on('dragmove', () => {
this.updateLineFromHandles(line, handles);
});
handle.on('dragend', () => {
// Snap edge handles to nearest device edge
if (isEdgeHandle) {
const snappedPos = this.snapToNearestDeviceEdge(handle.x(), handle.y());
handle.position(snappedPos);
this.updateLineFromHandles(line, handles);
}
this.saveWaypoints(connData.id, handles);
// After drag, check if mouse is still over line or handles to keep handles visible
const stage = this.connectionLayer.getStage();
const pointerPos = stage.getPointerPosition();
if (pointerPos) {
const shape = this.connectionLayer.getIntersection(pointerPos);
const isOverLineOrHandle = shape === line || handles.includes(shape);
if (!isOverLineOrHandle) {
// Mouse not over line or handles, hide handles and reset line style
line.stroke('#000000');
line.strokeWidth(1);
handles.forEach(h => h.opacity(0));
this.connectionLayer.batchDraw();
}
}
});
});
// Double-click on line to add waypoint
line.on('dblclick', (e) => {
const stage = this.connectionLayer.getStage();
const pointerPos = stage.getPointerPosition();
const scale = stage.scaleX();
const stagePos = stage.position();
// Convert to world coordinates
const worldX = (pointerPos.x - stagePos.x) / scale;
const worldY = (pointerPos.y - stagePos.y) / scale;
// Create new waypoint at double-click position
this.addWaypointAtPosition(line, handles, worldX, worldY, connData);
});
// Right-click to delete
line.on('contextmenu', (e) => {
e.evt.preventDefault();
this.showConnectionContextMenu(e, connData, line, handles);
});
// Update line when devices move
const updateLine = () => {
const newSourcePos = this.getDeviceAbsolutePosition(sourceShape);
const newTargetPos = this.getDeviceAbsolutePosition(targetShape);
const { sourcePoint: newSourcePoint, targetPoint: newTargetPoint } =
this.getEdgeConnectionPoints(newSourcePos, newTargetPos, this.deviceManager.deviceWidth, this.deviceManager.deviceHeight);
// Update first and last handle positions (start and end)
handles[0].position(newSourcePoint);
handles[handles.length - 1].position(newTargetPoint);
// Get current middle waypoints
const waypoints = handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
// Rebuild path with new endpoints and current waypoints
const newPoints = this.rebuildOrthogonalPath(newSourcePoint, waypoints, newTargetPoint);
line.points(newPoints);
this.connectionLayer.batchDraw();
};
// Attach update listeners based on current view
if (this.currentView === 'logical') {
// In logical view, devices are on main layer - attach listeners to devices
sourceShape.on('dragmove.connection', updateLine);
targetShape.on('dragmove.connection', updateLine);
} else {
// In physical view, devices are in racks - attach listeners to racks
const sourceRack = sourceShape.getParent().getParent();
const targetRack = targetShape.getParent().getParent();
if (sourceRack) sourceRack.on('dragmove.connection', updateLine);
if (targetRack) targetRack.on('dragmove.connection', updateLine);
}
// Line already added earlier (before handles, for correct z-order)
return { data: connData, shape: line, handles: handles, sourceShape, targetShape };
}
addWaypointAtPosition(line, handles, worldX, worldY, connData) {
const conn = this.connections.get(connData.id);
if (!conn) return;
// Get current points
const startPoint = { x: handles[0].x(), y: handles[0].y() };
const endPoint = { x: handles[handles.length - 1].x(), y: handles[handles.length - 1].y() };
const waypoints = handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
// Add new waypoint
waypoints.push({ x: worldX, y: worldY });
// Recreate handles
this.recreateHandles(line, handles, startPoint, waypoints, endPoint, connData);
// Show handles and highlight line (we just added a waypoint)
line.stroke('#4A90E2');
line.strokeWidth(2);
handles.forEach(h => h.opacity(1));
// Save
this.saveWaypoints(connData.id, handles);
this.connectionLayer.batchDraw();
}
recreateHandles(line, handles, startPoint, waypoints, endPoint, connData) {
// Destroy old handles
handles.forEach(h => h.destroy());
handles.length = 0;
// Rebuild path
const newPoints = this.rebuildOrthogonalPath(startPoint, waypoints, endPoint);
line.points(newPoints);
// Create new handles
const allPoints = [startPoint, ...waypoints, endPoint];
allPoints.forEach((pt, i) => {
const isEdgeHandle = (i === 0 || i === allPoints.length - 1);
const handle = new Konva.Circle({
x: pt.x,
y: pt.y,
radius: 4,
fill: '#4A90E2',
stroke: '#fff',
strokeWidth: 1,
draggable: true,
opacity: 0, // Hidden by default
name: `handle-${i}`
});
// Attach event listeners
handle.on('mouseenter', () => {
// Keep line highlighted and handles visible when hovering over handles
line.stroke('#4A90E2');
line.strokeWidth(2);
handles.forEach(h => h.opacity(1));
this.connectionLayer.batchDraw();
});
handle.on('mousedown', (e) => e.cancelBubble = true);
handle.on('dragmove', () => {
this.updateLineFromHandles(line, handles);
});
handle.on('dragend', () => {
if (isEdgeHandle) {
const snappedPos = this.snapToNearestDeviceEdge(handle.x(), handle.y());
handle.position(snappedPos);
this.updateLineFromHandles(line, handles);
}
this.saveWaypoints(connData.id, handles);
// After drag, check if mouse is still over line or handles to keep handles visible
const stage = this.connectionLayer.getStage();
const pointerPos = stage.getPointerPosition();
if (pointerPos) {
const shape = this.connectionLayer.getIntersection(pointerPos);
const isOverLineOrHandle = shape === line || handles.includes(shape);
if (!isOverLineOrHandle) {
// Mouse not over line or handles, hide handles and reset line style
line.stroke('#000000');
line.strokeWidth(1);
handles.forEach(h => h.opacity(0));
this.connectionLayer.batchDraw();
}
}
});
handles.push(handle);
this.connectionLayer.add(handle);
});
// Update connection reference
const conn = this.connections.get(connData.id);
if (conn) conn.handles = handles;
}
async saveWaypoints(connectionId, handles) {
try {
// Save ALL waypoints including edge points
const allPoints = handles.map((h, i) => ({
x: h.x(),
y: h.y(),
isEdge: i === 0 || i === handles.length - 1
}));
// Save to view-specific column
await this.api.updateConnectionWaypoints(connectionId, allPoints, this.currentView);
} catch (err) {
console.error('Failed to save waypoints:', err);
}
}
createSimpleOrthogonalPath(start, end) {
// Create a proper orthogonal path with right angles only
const dx = Math.abs(end.x - start.x);
const dy = Math.abs(end.y - start.y);
// If points are already aligned (same x or y), draw straight line
if (dx === 0 || dy === 0) {
return [start.x, start.y, end.x, end.y];
}
// Create L-shaped path based on which direction is dominant
if (dx > dy) {
// Horizontal first, then vertical
return [
start.x, start.y,
end.x, start.y,
end.x, end.y
];
} else {
// Vertical first, then horizontal
return [
start.x, start.y,
start.x, end.y,
end.x, end.y
];
}
}
snapToNearestDeviceEdge(x, y) {
const deviceWidth = this.deviceManager.deviceWidth;
const deviceHeight = this.deviceManager.deviceHeight;
let nearestEdge = null;
let minDistance = Infinity;
// Get all devices from deviceManager
this.deviceManager.devices.forEach((device, deviceId) => {
const deviceShape = device.shape;
if (!deviceShape) return;
const devicePos = this.getDeviceAbsolutePosition(deviceShape);
// Calculate all 4 edges
const edges = [
{
type: 'left',
x: devicePos.x,
y: Math.max(devicePos.y, Math.min(devicePos.y + deviceHeight, y)),
line: { x1: devicePos.x, y1: devicePos.y, x2: devicePos.x, y2: devicePos.y + deviceHeight }
},
{
type: 'right',
x: devicePos.x + deviceWidth,
y: Math.max(devicePos.y, Math.min(devicePos.y + deviceHeight, y)),
line: { x1: devicePos.x + deviceWidth, y1: devicePos.y, x2: devicePos.x + deviceWidth, y2: devicePos.y + deviceHeight }
},
{
type: 'top',
x: Math.max(devicePos.x, Math.min(devicePos.x + deviceWidth, x)),
y: devicePos.y,
line: { x1: devicePos.x, y1: devicePos.y, x2: devicePos.x + deviceWidth, y2: devicePos.y }
},
{
type: 'bottom',
x: Math.max(devicePos.x, Math.min(devicePos.x + deviceWidth, x)),
y: devicePos.y + deviceHeight,
line: { x1: devicePos.x, y1: devicePos.y + deviceHeight, x2: devicePos.x + deviceWidth, y2: devicePos.y + deviceHeight }
}
];
// Find nearest edge point
edges.forEach(edge => {
const dist = Math.sqrt(Math.pow(edge.x - x, 2) + Math.pow(edge.y - y, 2));
if (dist < minDistance) {
minDistance = dist;
nearestEdge = { x: edge.x, y: edge.y };
}
});
});
// Return snapped position or original if no devices found
return nearestEdge || { x, y };
}
getEdgeConnectionPoints(sourcePos, targetPos, deviceWidth, deviceHeight) {
// Calculate center points
const sourceCenterX = sourcePos.x + deviceWidth / 2;
const sourceCenterY = sourcePos.y + deviceHeight / 2;
const targetCenterX = targetPos.x + deviceWidth / 2;
const targetCenterY = targetPos.y + deviceHeight / 2;
const dx = targetCenterX - sourceCenterX;
const dy = targetCenterY - sourceCenterY;
let sourcePoint, targetPoint;
// Determine which edges to connect based on relative positions
if (Math.abs(dx) > Math.abs(dy)) {
// Primarily horizontal - use left/right edges
if (dx > 0) {
// Source right edge, target left edge
sourcePoint = { x: sourcePos.x + deviceWidth, y: sourceCenterY };
targetPoint = { x: targetPos.x, y: targetCenterY };
} else {
// Source left edge, target right edge
sourcePoint = { x: sourcePos.x, y: sourceCenterY };
targetPoint = { x: targetPos.x + deviceWidth, y: targetCenterY };
}
} else {
// Primarily vertical - use top/bottom edges
if (dy > 0) {
// Source bottom edge, target top edge
sourcePoint = { x: sourceCenterX, y: sourcePos.y + deviceHeight };
targetPoint = { x: targetCenterX, y: targetPos.y };
} else {
// Source top edge, target bottom edge
sourcePoint = { x: sourceCenterX, y: sourcePos.y };
targetPoint = { x: targetCenterX, y: targetPos.y + deviceHeight };
}
}
return { sourcePoint, targetPoint };
}
updateLineFromHandles(line, handles) {
if (handles.length < 2) return;
const startPoint = { x: handles[0].x(), y: handles[0].y() };
const endPoint = { x: handles[handles.length - 1].x(), y: handles[handles.length - 1].y() };
const waypoints = handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
const newPoints = this.rebuildOrthogonalPath(startPoint, waypoints, endPoint);
line.points(newPoints);
this.connectionLayer.batchDraw();
}
rebuildOrthogonalPath(start, waypoints, end) {
// Simple orthogonal path: just connect all points with right angles
const points = [start.x, start.y];
let prev = start;
const allPoints = [...waypoints, end];
allPoints.forEach(curr => {
const dx = Math.abs(curr.x - prev.x);
const dy = Math.abs(curr.y - prev.y);
// Add corner point if needed
if (dx > 0 && dy > 0) {
// Choose direction based on larger distance
if (dx > dy) {
points.push(curr.x, prev.y); // Horizontal first
} else {
points.push(prev.x, curr.y); // Vertical first
}
}
points.push(curr.x, curr.y);
prev = curr;
});
return points;
}
getDeviceAbsolutePosition(deviceShape) {
if (this.currentView === 'logical') {
// In logical view, devices are on main layer with absolute positioning
return {
x: deviceShape.x(),
y: deviceShape.y()
};
} else {
// In physical view, devices are in rack containers with relative positioning
const parent = deviceShape.getParent();
if (!parent || parent === this.layer) {
// Device is on main layer (shouldn't happen in physical view, but handle it)
return {
x: deviceShape.x(),
y: deviceShape.y()
};
}
const rack = parent.getParent();
return {
x: rack.x() + deviceShape.x(),
y: rack.y() + deviceShape.y()
};
}
}
updateAllConnections() {
// Update all connection paths
this.connections.forEach(conn => {
if (!conn.sourceShape || !conn.targetShape) return;
const deviceWidth = this.deviceManager.deviceWidth;
const deviceHeight = this.deviceManager.deviceHeight;
const sourcePos = this.getDeviceAbsolutePosition(conn.sourceShape);
const targetPos = this.getDeviceAbsolutePosition(conn.targetShape);
const { sourcePoint, targetPoint } = this.getEdgeConnectionPoints(
sourcePos, targetPos, deviceWidth, deviceHeight
);
// Update first and last handle positions (start and end)
conn.handles[0].position(sourcePoint);
conn.handles[conn.handles.length - 1].position(targetPoint);
// Get middle waypoint positions from handles
const waypoints = conn.handles.slice(1, -1).map(h => ({ x: h.x(), y: h.y() }));
// Rebuild path
const points = this.rebuildOrthogonalPath(sourcePoint, waypoints, targetPoint);
conn.shape.points(points);
});
this.connectionLayer.batchDraw();
}
startConnection(deviceId, deviceShape) {
// Cancel any existing connection mode
if (this.tempLine) {
this.tempLine.destroy();
this.tempLine = null;
}
const deviceData = this.deviceManager.getDeviceData(deviceId);
if (!deviceData) return;
const deviceWidth = this.deviceManager.deviceWidth;
const deviceHeight = this.deviceManager.deviceHeight;
const deviceAbsPos = this.getDeviceAbsolutePosition(deviceShape);
// Start from device center (will be adjusted to edge when connection completes)
const startPoint = {
x: deviceAbsPos.x + deviceWidth / 2,
y: deviceAbsPos.y + deviceHeight / 2
};
// Create temporary line
this.tempLine = new Konva.Line({
points: [startPoint.x, startPoint.y, startPoint.x, startPoint.y],
stroke: '#000000',
strokeWidth: 1,
dash: [10, 5],
listening: false
});
this.connectionLayer.add(this.tempLine);
this.tempLine.moveToTop();
this.pendingConnection = {
sourceDeviceId: deviceId,
sourceDeviceShape: deviceShape,
sourceDeviceData: deviceData,
startPoint: startPoint,
deviceAbsPos: deviceAbsPos
};
// Update temp line on mouse move
const stage = this.connectionLayer.getStage();
stage.on('mousemove.connection', () => {
if (this.tempLine && this.pendingConnection) {
const pos = stage.getPointerPosition();
const scale = stage.scaleX();
const stagePos = stage.position();
const worldX = (pos.x - stagePos.x) / scale;
const worldY = (pos.y - stagePos.y) / scale;
// Calculate which edge to use based on cursor position
const dx = worldX - (this.pendingConnection.deviceAbsPos.x + deviceWidth / 2);
const dy = worldY - (this.pendingConnection.deviceAbsPos.y + deviceHeight / 2);
let edgePoint;
if (Math.abs(dx) > Math.abs(dy)) {
// Use left or right edge
if (dx > 0) {
edgePoint = {
x: this.pendingConnection.deviceAbsPos.x + deviceWidth,
y: this.pendingConnection.deviceAbsPos.y + deviceHeight / 2
};
} else {
edgePoint = {
x: this.pendingConnection.deviceAbsPos.x,
y: this.pendingConnection.deviceAbsPos.y + deviceHeight / 2
};
}
} else {
// Use top or bottom edge
if (dy > 0) {
edgePoint = {
x: this.pendingConnection.deviceAbsPos.x + deviceWidth / 2,
y: this.pendingConnection.deviceAbsPos.y + deviceHeight
};
} else {
edgePoint = {
x: this.pendingConnection.deviceAbsPos.x + deviceWidth / 2,
y: this.pendingConnection.deviceAbsPos.y
};
}
}
this.tempLine.points([edgePoint.x, edgePoint.y, worldX, worldY]);
this.connectionLayer.batchDraw();
}
});
}
async completeConnection(targetDeviceId, targetDeviceShape) {
if (!this.pendingConnection) return;
const sourceDeviceId = this.pendingConnection.sourceDeviceId;
if (sourceDeviceId === targetDeviceId) {
this.cancelConnection();
return;
}
try {
const sourceUsedPorts = await this.api.getUsedPorts(sourceDeviceId);
const targetUsedPorts = await this.api.getUsedPorts(targetDeviceId);
const sourcePort = this.getNextAvailablePort(this.pendingConnection.sourceDeviceData, sourceUsedPorts);
const targetData = this.deviceManager.getDeviceData(targetDeviceId);
const targetPort = this.getNextAvailablePort(targetData, targetUsedPorts);
if (sourcePort === null || targetPort === null) {
this.cancelConnection();
return;
}
const connData = await this.api.createConnection(
sourceDeviceId,
sourcePort,
targetDeviceId,
targetPort
);
const connections = await this.api.getConnections();
const newConn = connections.find(c => c.id === connData.id);
if (newConn) {
this.createConnectionLine(newConn);
this.connectionLayer.batchDraw();
}
} catch (err) {
console.error('Failed to create connection:', err);
}
this.cancelConnection();
}
getNextAvailablePort(deviceData, usedPorts) {
const portsCount = deviceData.ports_count || 24;
for (let port = 1; port <= portsCount; port++) {
if (!usedPorts.includes(port)) {
return port;
}
}
return null;
}
cancelConnection() {
if (this.tempLine) {
this.tempLine.destroy();
this.tempLine = null;
}
const stage = this.connectionLayer.getStage();
if (stage) {
stage.off('mousemove.connection');
}
this.pendingConnection = null;
this.connectionLayer.batchDraw();
}
async deleteConnection(connId, line, handles) {
try {
await this.api.deleteConnection(connId);
line.destroy();
handles.forEach(h => h.destroy());
this.connections.delete(connId);
// Clear selection if this was the selected connection
if (this.selectedConnection === connId) {
this.selectedConnection = null;
}
this.connectionLayer.batchDraw();
// Notify table to sync
window.dispatchEvent(new CustomEvent('canvas-data-changed'));
} catch (err) {
console.error('Failed to delete connection:', err);
}
}
showConnectionContextMenu(e, connData, line, handles) {
const contextMenu = document.getElementById('contextMenu');
const contextMenuList = document.getElementById('contextMenuList');
const sourceDevice = this.deviceManager.getDeviceData(connData.source_device_id);
const targetDevice = this.deviceManager.getDeviceData(connData.target_device_id);
contextMenuList.innerHTML = `
<li class="menu-header">Connection</li>
<li style="cursor: default; padding: 8px 20px; font-size: 12px; color: #666;">
${sourceDevice.name}:${connData.source_port}${targetDevice.name}:${connData.target_port}
</li>
<li class="divider"></li>
<li data-action="delete">Delete Connection</li>
`;
contextMenu.style.left = `${e.evt.pageX}px`;
contextMenu.style.top = `${e.evt.pageY}px`;
contextMenu.classList.remove('hidden');
const handleAction = (evt) => {
const action = evt.target.dataset.action;
if (action === 'delete') {
if (confirm('Delete this connection?')) {
this.deleteConnection(connData.id, line, handles);
}
}
contextMenu.classList.add('hidden');
contextMenuList.removeEventListener('click', handleAction);
};
contextMenuList.addEventListener('click', handleAction);
}
}