|
|
|
@ -1,5 +1,12 @@ |
|
|
|
import { ComponentNode } from './node.js' |
|
|
|
import { ComponentNode } from './node.js' |
|
|
|
import { generateNodeId, createSVGElement } from './utils.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 { PluginRegistry } from './pluginRegistry.js'; |
|
|
|
|
|
|
|
|
|
|
|
export class CanvasApp { |
|
|
|
export class CanvasApp { |
|
|
|
constructor() { |
|
|
|
constructor() { |
|
|
|
@ -23,21 +30,11 @@ export class CanvasApp { |
|
|
|
this.dbGroup = document.getElementById('db-group'); |
|
|
|
this.dbGroup = document.getElementById('db-group'); |
|
|
|
this.cacheGroup = document.getElementById('cache-group'); |
|
|
|
this.cacheGroup = document.getElementById('cache-group'); |
|
|
|
this.selectedNode = null; |
|
|
|
this.selectedNode = null; |
|
|
|
this.computeTypes = ['webserver', 'microservice']; |
|
|
|
|
|
|
|
this.computeGroup = document.getElementById('compute-group'); |
|
|
|
this.computeGroup = document.getElementById('compute-group'); |
|
|
|
|
|
|
|
this.lbGroup = document.getElementById('lb-group'); |
|
|
|
|
|
|
|
this.mqGroup = document.getElementById('mq-group'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.placeholderText = createSVGElement('text', { |
|
|
|
|
|
|
|
x: '50%', |
|
|
|
|
|
|
|
y: '50%', |
|
|
|
|
|
|
|
'text-anchor': 'middle', |
|
|
|
|
|
|
|
'dominant-baseline': 'middle', |
|
|
|
|
|
|
|
fill: '#444', |
|
|
|
|
|
|
|
'font-size': 18, |
|
|
|
|
|
|
|
'pointer-events': 'none' |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
this.placeholderText.textContent = 'Drag and drop elements to start building your system. Press backspace or delete to remove elements.'; |
|
|
|
|
|
|
|
this.canvas.appendChild(this.placeholderText); |
|
|
|
|
|
|
|
this.initEventHandlers(); |
|
|
|
this.initEventHandlers(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -56,10 +53,12 @@ export class CanvasApp { |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
this.sidebar.addEventListener('dragstart', (e) => { |
|
|
|
this.sidebar.addEventListener('dragstart', (e) => { |
|
|
|
if (e.target.classList.contains('component-icon')) { |
|
|
|
const type = e.target.getAttribute('data-type'); |
|
|
|
e.dataTransfer.setData('text/plain', e.target.getAttribute('data-type')); |
|
|
|
const plugin = PluginRegistry.get(type); |
|
|
|
e.target.classList.add('dragging'); |
|
|
|
|
|
|
|
} |
|
|
|
if (!plugin) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
e.dataTransfer.setData('text/plain', type) |
|
|
|
}); |
|
|
|
}); |
|
|
|
this.sidebar.addEventListener('dragend', (e) => { |
|
|
|
this.sidebar.addEventListener('dragend', (e) => { |
|
|
|
if (e.target.classList.contains('component-icon')) { |
|
|
|
if (e.target.classList.contains('component-icon')) { |
|
|
|
@ -69,26 +68,27 @@ export class CanvasApp { |
|
|
|
|
|
|
|
|
|
|
|
this.canvasContainer.addEventListener('dragover', (e) => e.preventDefault()); |
|
|
|
this.canvasContainer.addEventListener('dragover', (e) => e.preventDefault()); |
|
|
|
this.canvasContainer.addEventListener('drop', (e) => { |
|
|
|
this.canvasContainer.addEventListener('drop', (e) => { |
|
|
|
e.preventDefault(); |
|
|
|
|
|
|
|
const type = e.dataTransfer.getData('text/plain'); |
|
|
|
const type = e.dataTransfer.getData('text/plain'); |
|
|
|
|
|
|
|
const plugin = PluginRegistry.get(type); |
|
|
|
|
|
|
|
if (!plugin) return; |
|
|
|
|
|
|
|
|
|
|
|
const pt = this.canvas.createSVGPoint(); |
|
|
|
const pt = this.canvas.createSVGPoint(); |
|
|
|
pt.x = e.clientX; |
|
|
|
pt.x = e.clientX; |
|
|
|
pt.y = e.clientY; |
|
|
|
pt.y = e.clientY; |
|
|
|
|
|
|
|
|
|
|
|
const svgP = pt.matrixTransform(this.canvas.getScreenCTM().inverse()); |
|
|
|
const svgP = pt.matrixTransform(this.canvas.getScreenCTM().inverse()); |
|
|
|
const x = svgP.x - this.componentSize.width / 2; |
|
|
|
const x = svgP.x - this.componentSize.width / 2; |
|
|
|
const y = svgP.y - this.componentSize.height / 2; |
|
|
|
const y = svgP.y - this.componentSize.height / 2; |
|
|
|
const node = new ComponentNode(type, x, y, this); |
|
|
|
|
|
|
|
|
|
|
|
const props = generateDefaultProps(plugin); |
|
|
|
|
|
|
|
const node = new ComponentNode(type, x, y, this, props); |
|
|
|
node.x = x; |
|
|
|
node.x = x; |
|
|
|
node.y = y; |
|
|
|
node.y = y; |
|
|
|
if (this.placeholderText) { |
|
|
|
|
|
|
|
this.placeholderText.remove(); |
|
|
|
|
|
|
|
this.placeholderText = null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.runButton.addEventListener('click', () => { |
|
|
|
this.runButton.addEventListener('click', () => { |
|
|
|
const designData = this.exportDesign(); |
|
|
|
const designData = this.exportDesign(); |
|
|
|
console.log(JSON.stringify(designData)); |
|
|
|
console.log(JSON.stringify(designData)) |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.canvas.addEventListener('click', () => { |
|
|
|
this.canvas.addEventListener('click', () => { |
|
|
|
@ -102,21 +102,35 @@ export class CanvasApp { |
|
|
|
|
|
|
|
|
|
|
|
this.propsSaveBtn.addEventListener('click', () => { |
|
|
|
this.propsSaveBtn.addEventListener('click', () => { |
|
|
|
if (!this.activeNode) return; |
|
|
|
if (!this.activeNode) return; |
|
|
|
const nodeObj = this.activeNode; |
|
|
|
|
|
|
|
|
|
|
|
const node = this.activeNode; |
|
|
|
const panel = this.nodePropsPanel; |
|
|
|
const panel = this.nodePropsPanel; |
|
|
|
const newLabel = panel.querySelector("input[name='label']").value; |
|
|
|
const plugin = PluginRegistry.get(node.type); |
|
|
|
nodeObj.updateLabel(newLabel); |
|
|
|
|
|
|
|
if (nodeObj.type === 'Database') { |
|
|
|
if (!plugin || !plugin.props) { |
|
|
|
nodeObj.props.replication = parseInt(panel.querySelector("input[name='replication']").value, 10); |
|
|
|
this.hidePropsPanel(); |
|
|
|
} |
|
|
|
return; |
|
|
|
if (nodeObj.type === 'cache') { |
|
|
|
|
|
|
|
nodeObj.props.cacheTTL = parseInt(panel.querySelector("input[name='cacheTTL']").value, 10); |
|
|
|
|
|
|
|
nodeObj.props.maxEntries = parseInt(panel.querySelector("input[name='maxEntries']").value, 10); |
|
|
|
|
|
|
|
nodeObj.props.evictionPolicy = panel.querySelector("select[name='evictionPolicy']").value; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
if (this.computeTypes.includes(nodeObj.type)) { |
|
|
|
|
|
|
|
nodeObj.props.instanceSize = panel.querySelector("select[name='instanceSize']").value; |
|
|
|
// 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(); |
|
|
|
this.hidePropsPanel(); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
@ -147,54 +161,47 @@ export class CanvasApp { |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
showPropsPanel(nodeObj) { |
|
|
|
showPropsPanel(node) { |
|
|
|
this.activeNode = nodeObj; |
|
|
|
this.activeNode = node; |
|
|
|
|
|
|
|
const plugin = PluginRegistry.get(node.type); |
|
|
|
const panel = this.nodePropsPanel; |
|
|
|
const panel = this.nodePropsPanel; |
|
|
|
|
|
|
|
|
|
|
|
if (nodeObj.type === 'user') { |
|
|
|
if (!plugin || this.arrowMode) { |
|
|
|
this.hidePropsPanel(); |
|
|
|
this.hidePropsPanel(); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Position the panel (optional, or you can use fixed top/right)
|
|
|
|
const bbox = node.group.getBBox(); |
|
|
|
const bbox = nodeObj.group.getBBox(); |
|
|
|
const ctm = node.group.getCTM(); |
|
|
|
const ctm = nodeObj.group.getCTM(); |
|
|
|
|
|
|
|
const screenX = ctm.e + bbox.x; |
|
|
|
const screenX = ctm.e + bbox.x; |
|
|
|
const screenY = ctm.f + bbox.y + bbox.height; |
|
|
|
const screenY = ctm.f + bbox.y + bbox.height; |
|
|
|
panel.style.left = (screenX + this.canvasContainer.getBoundingClientRect().left) + 'px'; |
|
|
|
panel.style.left = (screenX + this.canvasContainer.getBoundingClientRect().left) + 'px'; |
|
|
|
panel.style.top = (screenY + this.canvasContainer.getBoundingClientRect().top) + 'px'; |
|
|
|
panel.style.top = (screenY + this.canvasContainer.getBoundingClientRect().top) + 'px'; |
|
|
|
|
|
|
|
|
|
|
|
// Always show label group
|
|
|
|
// Hide all groups first
|
|
|
|
this.labelGroup.style.display = 'block'; |
|
|
|
const allGroups = panel.querySelectorAll('.prop-group, #label-group, #compute-group, #lb-group'); |
|
|
|
panel.querySelector("input[name='label']").value = nodeObj.props.label; |
|
|
|
allGroups.forEach(g => g.style.display = 'none'); |
|
|
|
|
|
|
|
|
|
|
|
// Show DB fields if it's a Database
|
|
|
|
const shownGroups = new Set(); |
|
|
|
this.dbGroup.style.display = nodeObj.type === 'Database' ? 'block' : 'none'; |
|
|
|
|
|
|
|
if (nodeObj.type === 'Database') { |
|
|
|
for (const prop of plugin.props) { |
|
|
|
this.dbGroup.querySelector("input[name='replication']").value = nodeObj.props.replication; |
|
|
|
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); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Show cache fields if it's a cache
|
|
|
|
// Set value
|
|
|
|
const isCache = nodeObj.type === 'cache'; |
|
|
|
if (input) { |
|
|
|
this.cacheGroup.style.display = isCache ? 'block' : 'none'; |
|
|
|
input.value = node.props[prop.name] ?? prop.default; |
|
|
|
if (isCache) { |
|
|
|
} |
|
|
|
this.cacheGroup.querySelector("input[name='cacheTTL']").value = nodeObj.props.cacheTTL ?? 0; |
|
|
|
|
|
|
|
panel.querySelector('input[name="cacheTTL"]').value = nodeObj.props.cacheTTL; |
|
|
|
|
|
|
|
panel.querySelector("input[name='maxEntries']").value = nodeObj.props.maxEntries || 100000; |
|
|
|
|
|
|
|
panel.querySelector("select[name='evictionPolicy']").value = nodeObj.props.evictionPolicy || 'LRU'; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.propsSaveBtn.disabled = false; |
|
|
|
this.propsSaveBtn.disabled = false; |
|
|
|
panel.style.display = 'block'; |
|
|
|
panel.style.display = 'block'; |
|
|
|
|
|
|
|
|
|
|
|
const isCompute = this.computeTypes.includes(nodeObj.type); |
|
|
|
|
|
|
|
console.log(isCompute) |
|
|
|
|
|
|
|
console.log(nodeObj) |
|
|
|
|
|
|
|
this.computeGroup.style.display = isCompute ? 'block' : 'none'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isCompute) { |
|
|
|
|
|
|
|
panel.querySelector("select[name='instanceSize']").value = nodeObj.props.instanceSize || 'medium'; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
hidePropsPanel() { |
|
|
|
hidePropsPanel() { |
|
|
|
@ -228,35 +235,19 @@ export class CanvasApp { |
|
|
|
const nodes = this.placedComponents |
|
|
|
const nodes = this.placedComponents |
|
|
|
.filter(n => n.type !== 'user') |
|
|
|
.filter(n => n.type !== 'user') |
|
|
|
.map(n => { |
|
|
|
.map(n => { |
|
|
|
const baseProps = { label: n.props.label }; |
|
|
|
const plugin = PluginRegistry.get(n.type); |
|
|
|
|
|
|
|
const result = { |
|
|
|
// Add only if it's a database
|
|
|
|
|
|
|
|
if (n.type === 'Database') { |
|
|
|
|
|
|
|
baseProps.replication = n.props.replication; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add only if it's a cache
|
|
|
|
|
|
|
|
if (n.type === 'cache') { |
|
|
|
|
|
|
|
baseProps.cacheTTL = n.props.cacheTTL; |
|
|
|
|
|
|
|
baseProps.maxEntries = n.props.maxEntries; |
|
|
|
|
|
|
|
baseProps.evictionPolicy = n.props.evictionPolicy; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add only if it's a compute node
|
|
|
|
|
|
|
|
const computeTypes = ['WebServer', 'Microservice']; |
|
|
|
|
|
|
|
if (computeTypes.includes(n.type)) { |
|
|
|
|
|
|
|
baseProps.instanceSize = n.props.instanceSize; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
|
|
id: n.id, |
|
|
|
id: n.id, |
|
|
|
type: n.type, |
|
|
|
type: n.type, |
|
|
|
props: baseProps, |
|
|
|
position: { x: n.x, y: n.y }, |
|
|
|
position: { |
|
|
|
props: {} |
|
|
|
x: n.x, |
|
|
|
|
|
|
|
y: n.y |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plugin?.props?.forEach(p => { |
|
|
|
|
|
|
|
result.props[p.name] = n.props[p.name]; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const connections = this.connections.map(c => ({ |
|
|
|
const connections = this.connections.map(c => ({ |
|
|
|
|