You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

279 lines
9.7 KiB

import { ComponentNode } from './node.js'
import { generateNodeId, createSVGElement, generateDefaultProps } from './utils.js';
import './plugins/user.js';
import './plugins/webserver.js';
import './plugins/cache.js';
import './plugins/loadbalancer.js';
import './plugins/database.js';
import './plugins/messageQueue.js';
import './plugins/cdn.js';
import './plugins/microservice.js';
import './plugins/datapipeline.js';
import './plugins/monitorAlerting.js';
import './plugins/thirdPartyService.js';
import { PluginRegistry } from './pluginRegistry.js';
import { initializeObservers } from './observers.js';
import {
CommandInvoker,
SwitchToResourcesTabCommand,
SwitchToDesignTabCommand,
ToggleArrowModeCommand,
StartChatCommand,
SendChatMessageCommand,
HandleDragStartCommand,
HandleDragEndCommand,
DropComponentCommand,
RunSimulationCommand,
HandleCanvasClickCommand,
SaveNodePropertiesCommand,
DeleteSelectionCommand
} from './commands.js';
import { CanvasStateMachine } from './states/CanvasStateMachine.js';
export class CanvasApp {
constructor() {
this.placedComponents = [];
this.connections = [];
this.componentSize = { width: 120, height: 40 };
this.arrowMode = false;
this.connectionStart = null;
this.pendingConnection = null;
this.activeNode = null;
this.selectedConnection = null;
this.sidebar = document.getElementById('sidebar');
this.arrowToolBtn = document.getElementById('arrow-tool-btn');
this.canvasContainer = document.getElementById('canvas-container');
this.canvas = document.getElementById('canvas');
this.runButton = document.getElementById('run-button');
this.nodePropsPanel = document.getElementById('node-props-panel');
this.propsSaveBtn = document.getElementById('node-props-save');
this.labelGroup = document.getElementById('label-group');
this.dbGroup = document.getElementById('db-group');
this.cacheGroup = document.getElementById('cache-group');
this.selectedNode = null;
this.computeGroup = document.getElementById('compute-group');
this.lbGroup = document.getElementById('lb-group');
this.mqGroup = document.getElementById('mq-group');
this.startChatBtn = document.getElementById('start-chat');
this.chatElement = document.getElementById('chat-box');
this.chatTextField = document.getElementById('chat-message-box');
this.chatMessages = document.getElementById('messages');
this.chatLoadingIndicator = document.getElementById('loading-indicator');
this.level = window.levelData;
this.ws = null;
this.plugins = PluginRegistry.getAll()
this.createDesignBtn = document.getElementById('create-design-button');
this.learnMoreBtn = document.getElementById('learn-more-button');
this.tabs = document.getElementsByClassName('tabinput');
this._reconnectDelay = 1000;
this._maxReconnectDelay = 15000;
this._reconnectTimer = null;
// Initialize observer system (alongside existing event handling)
const observers = initializeObservers(this.nodePropsPanel, this.propsSaveBtn);
this.propertiesPanelSubject = observers.propertiesPanel;
this.nodeSelectionSubject = observers.nodeSelection;
this.connectionModeSubject = observers.connectionMode;
// Initialize command system
this.commandInvoker = new CommandInvoker(this);
// Initialize state machine
this.stateMachine = new CanvasStateMachine(this);
this.initEventHandlers();
}
initEventHandlers() {
const requirementstab = this.tabs[1];
const designtab = this.tabs[1];
const resourcestab = this.tabs[2];
this.learnMoreBtn.addEventListener('click', () => {
this.commandInvoker.execute(new SwitchToResourcesTabCommand());
});
this.createDesignBtn.addEventListener('click', () => {
this.commandInvoker.execute(new SwitchToDesignTabCommand());
});
this.arrowToolBtn.addEventListener('click', () => {
this.commandInvoker.execute(new ToggleArrowModeCommand());
});
this.startChatBtn.addEventListener('click', () => {
this.commandInvoker.execute(new StartChatCommand());
});
this.chatTextField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
console.log('you sent a message');
const message = this.chatTextField.value;
if (message.trim()) {
this.commandInvoker.execute(new SendChatMessageCommand(message));
}
}
});
this.sidebar.addEventListener('dragstart', (e) => {
this.commandInvoker.execute(new HandleDragStartCommand(e));
});
this.sidebar.addEventListener('dragend', (e) => {
this.commandInvoker.execute(new HandleDragEndCommand(e));
});
this.canvasContainer.addEventListener('dragover', (e) => e.preventDefault());
this.canvasContainer.addEventListener('drop', (e) => {
this.commandInvoker.execute(new DropComponentCommand(e));
});
this.runButton.addEventListener('click', () => {
this.commandInvoker.execute(new RunSimulationCommand());
});
this.canvas.addEventListener('click', (e) => {
this.commandInvoker.execute(new HandleCanvasClickCommand(e));
});
this.propsSaveBtn.addEventListener('click', () => {
this.commandInvoker.execute(new SaveNodePropertiesCommand());
});
// Prevent props panel from closing when clicking inside it
this.nodePropsPanel.addEventListener('click', (e) => {
e.stopPropagation();
});
document.addEventListener('keydown', (e) => {
this.commandInvoker.execute(new DeleteSelectionCommand(e.key));
});
}
updateConnectionsFor(movedNode) {
this.connections.forEach(conn => {
if (conn.start === movedNode || conn.end === movedNode) {
conn.updatePosition();
}
});
}
exportDesign() {
const nodes = this.placedComponents
.map(n => {
const plugin = PluginRegistry.get(n.type);
const result = {
id: n.id,
type: n.type,
position: { x: n.x, y: n.y },
props: {}
};
plugin?.props?.forEach(p => {
result.props[p.name] = n.props[p.name];
});
return result;
});
const connections = this.connections.map(c => ({
source: c.start.id,
target: c.end.id,
label: c.label || '',
direction: c.direction,
protocol: c.protocol || '',
tls: !!c.tls,
capacity: c.capacity || 1000
}));
return {
nodes,
connections,
level: this.level,
availableComponents: this.plugins
};
}
getLevelInfo() {
// Try to extract level info from URL path like /play/url-shortener
const pathParts = window.location.pathname.split('/');
if (pathParts.length >= 3 && pathParts[1] === 'play') {
const levelId = decodeURIComponent(pathParts[2]);
return {
levelId: levelId
};
}
return {};
}
// showResults function removed - now handled by redirect to success/failure pages
showError(errorMessage) {
alert(`Simulation Error:\n\n${errorMessage}\n\nPlease check your design and try again.`);
}
_initWebSocket() {
const scheme = location.protocol === "https:" ? "wss://" : "ws://";
this.ws = new WebSocket(scheme + location.host + "/ws");
this.ws.onopen = () => {
console.log("WebSocket connected");
// Reset reconnection delay on successful connection
this._reconnectDelay = 1000;
this.ws.send(JSON.stringify({
'designPayload': JSON.stringify(this.exportDesign()),
'message': ''
}));
};
this.ws.onmessage = (e) => {
this.chatLoadingIndicator.style.display = 'none';
this.chatTextField.disabled = false;
this.chatTextField.focus();
const message = document.createElement('p');
message.innerHTML = e.data;
message.className = "other";
this.chatMessages.insertBefore(message, this.chatLoadingIndicator);
};
this.ws.onerror = (err) => {
console.log("ws error:", err);
this._scheduleReconnect();
};
this.ws.onclose = () => {
console.log("WebSocket closed, scheduling reconnect...");
this.ws = null;
this._scheduleReconnect();
};
}
_scheduleReconnect() {
if (this._stopped) return;
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null;
}
const jitter = this._reconnectDelay * (Math.random() * 0.4 - 0.2);
const delay = Math.max(250, Math.min(this._maxReconnectDelay, this._reconnectDelay + jitter));
console.log(`Reconnecting websocket in ${delay}ms...`)
this._reconnectTimer = setTimeout(() => {
this._reconnectTimer = null;
this._initWebSocket();
}, delay);
this._reconnectDelay = Math.min(this._maxReconnectDelay, Math.round(this._reconnectDelay * 1.8));
}
}