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.
 
 
 
 

368 lines
13 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';
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.initEventHandlers();
}
initEventHandlers() {
this.arrowToolBtn.addEventListener('click', () => {
this.arrowMode = !this.arrowMode;
if (this.arrowMode) {
this.arrowToolBtn.classList.add('active');
this.hidePropsPanel();
} else {
this.arrowToolBtn.classList.remove('active');
if (this.connectionStart) {
this.connectionStart.group.classList.remove('selected');
this.connectionStart = null;
}
}
});
this.sidebar.addEventListener('dragstart', (e) => {
const type = e.target.getAttribute('data-type');
const plugin = PluginRegistry.get(type);
if (!plugin) return;
e.dataTransfer.setData('text/plain', type)
});
this.sidebar.addEventListener('dragend', (e) => {
if (e.target.classList.contains('component-icon')) {
e.target.classList.remove('dragging');
}
});
this.canvasContainer.addEventListener('dragover', (e) => e.preventDefault());
this.canvasContainer.addEventListener('drop', (e) => {
const type = e.dataTransfer.getData('text/plain');
const plugin = PluginRegistry.get(type);
if (!plugin) return;
const pt = this.canvas.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
const svgP = pt.matrixTransform(this.canvas.getScreenCTM().inverse());
const x = svgP.x - this.componentSize.width / 2;
const y = svgP.y - this.componentSize.height / 2;
const props = generateDefaultProps(plugin);
const node = new ComponentNode(type, x, y, this, props);
node.x = x;
node.y = y;
});
this.runButton.addEventListener('click', async () => {
const designData = this.exportDesign();
// Try to get level info from URL or page context
const levelInfo = this.getLevelInfo();
const requestBody = {
design: designData,
...levelInfo
};
console.log('Sending design to simulation:', JSON.stringify(requestBody));
// Disable button and show loading state
this.runButton.disabled = true;
this.runButton.textContent = 'Running Simulation...';
try {
const response = await fetch('/simulate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.Success) {
console.log('Simulation successful:', result);
this.showResults(result);
} else {
console.error('Simulation failed:', result.Error);
this.showError(result.Error || 'Simulation failed');
}
} catch (error) {
console.error('Network error:', error);
this.showError('Failed to run simulation: ' + error.message);
} finally {
// Re-enable button
this.runButton.disabled = false;
this.runButton.textContent = 'Test Design';
}
});
this.canvas.addEventListener('click', () => {
if (this.connectionStart) {
this.connectionStart.group.classList.remove('selected');
this.connectionStart = null;
}
this.hidePropsPanel();
this.clearSelection();
});
this.propsSaveBtn.addEventListener('click', () => {
if (!this.activeNode) return;
const node = this.activeNode;
const panel = this.nodePropsPanel;
const plugin = PluginRegistry.get(node.type);
if (!plugin || !plugin.props) {
this.hidePropsPanel();
return;
}
// Loop through plugin-defined props and update the node
for (const prop of plugin.props) {
const input = panel.querySelector(`[name='${prop.name}']`);
if (!input) continue;
let value;
if (prop.type === 'number') {
value = parseFloat(input.value);
if (isNaN(value)) value = prop.default ?? 0;
} else {
value = input.value;
}
node.props[prop.name] = value;
if (prop.name === 'label') {
node.updateLabel(value);
}
}
this.hidePropsPanel();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
if (this.selectedConnection) {
this.canvas.removeChild(this.selectedConnection.line);
this.canvas.removeChild(this.selectedConnection.text);
const index = this.connections.indexOf(this.selectedConnection);
if (index !== -1) this.connections.splice(index, 1);
this.selectedConnection = null;
} else if (this.selectedNode) {
this.canvas.removeChild(this.selectedNode.group);
this.placedComponents = this.placedComponents.filter(n => n !== this.selectedNode);
this.connections = this.connections.filter(conn => {
if (conn.start === this.selectedNode || conn.end === this.selectedNode) {
this.canvas.removeChild(conn.line);
this.canvas.removeChild(conn.text);
return false;
}
return true;
});
this.selectedNode = null;
this.activeNode = null;
this.hidePropsPanel();
}
}
});
}
showPropsPanel(node) {
this.activeNode = node;
const plugin = PluginRegistry.get(node.type);
const panel = this.nodePropsPanel;
if (!plugin || this.arrowMode) {
this.hidePropsPanel();
return;
}
const bbox = node.group.getBBox();
const ctm = node.group.getCTM();
const screenX = ctm.e + bbox.x;
const screenY = ctm.f + bbox.y + bbox.height;
panel.style.left = (screenX + this.canvasContainer.getBoundingClientRect().left) + 'px';
panel.style.top = (screenY + this.canvasContainer.getBoundingClientRect().top) + 'px';
// Hide all groups first
const allGroups = panel.querySelectorAll('.prop-group, #label-group, #compute-group, #lb-group');
allGroups.forEach(g => g.style.display = 'none');
const shownGroups = new Set();
for (const prop of plugin.props) {
const group = panel.querySelector(`[data-group='${prop.group}']`);
const input = panel.querySelector(`[name='${prop.name}']`);
// Show group once
if (group && !shownGroups.has(group)) {
group.style.display = 'block';
shownGroups.add(group);
}
// Set value
if (input) {
input.value = node.props[prop.name] ?? prop.default;
}
}
this.propsSaveBtn.disabled = false;
panel.style.display = 'block';
}
hidePropsPanel() {
this.nodePropsPanel.style.display = 'none';
this.propsSaveBtn.disabled = true;
this.activeNode = null;
}
updateConnectionsFor(movedNode) {
this.connections.forEach(conn => {
if (conn.start === movedNode || conn.end === movedNode) {
conn.updatePosition();
}
});
}
clearSelection() {
if (this.selectedConnection) {
this.selectedConnection.deselect();
this.selectedConnection = null;
}
if (this.selectedNode) {
this.selectedNode.deselect();
this.selectedNode = null;
this.hidePropsPanel();
}
}
exportDesign() {
const nodes = this.placedComponents
.filter(n => n.type !== 'user')
.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 };
}
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 levelName = decodeURIComponent(pathParts[2]);
return {
levelName: levelName,
difficulty: 'easy' // Default difficulty, could be enhanced later
};
}
return {};
}
showResults(result) {
const metrics = result.Metrics;
let message = '';
// Level validation results
if (result.LevelName) {
if (result.Passed) {
message += `Level "${result.LevelName}" PASSED!\n`;
message += `Score: ${result.Score}/100\n\n`;
} else {
message += `Level "${result.LevelName}" FAILED\n`;
message += `Score: ${result.Score}/100\n\n`;
}
// Add detailed feedback
if (result.Feedback && result.Feedback.length > 0) {
message += result.Feedback.join('\n') + '\n\n';
}
} else {
message += `Simulation Complete!\n\n`;
}
// Performance metrics
message += `Performance Metrics:\n`;
message += `• Throughput: ${metrics.throughput} req/sec\n`;
message += `• Avg Latency: ${metrics.latency_avg}ms\n`;
message += `• Availability: ${metrics.availability.toFixed(1)}%\n`;
message += `• Monthly Cost: $${metrics.cost_monthly}\n\n`;
message += `Timeline: ${result.Timeline.length} ticks simulated`;
alert(message);
// TODO: Later replace with redirect to results page or modal
console.log('Full simulation data:', result);
}
showError(errorMessage) {
alert(`Simulation Error:\n\n${errorMessage}\n\nPlease check your design and try again.`);
}
}