From ed3705a9defaa27a6904590a13e81a8e690d12e8 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Mon, 9 Jun 2025 20:33:48 -0700 Subject: [PATCH] major refactor to plugin architecture; some plugins still need to be added --- static/app.js | 177 ++++++++++++++++----------------- static/game.html | 38 +++---- static/node.js | 37 +++++-- static/pluginRegistry.js | 11 ++ static/plugins/cache.js | 13 +++ static/plugins/database.js | 10 ++ static/plugins/loadbalancer.js | 10 ++ static/plugins/messageQueue.js | 11 ++ static/plugins/user.js | 10 ++ static/plugins/webserver.js | 10 ++ static/utils.js | 8 ++ 11 files changed, 217 insertions(+), 118 deletions(-) create mode 100644 static/pluginRegistry.js create mode 100644 static/plugins/cache.js create mode 100644 static/plugins/database.js create mode 100644 static/plugins/loadbalancer.js create mode 100644 static/plugins/messageQueue.js create mode 100644 static/plugins/user.js create mode 100644 static/plugins/webserver.js diff --git a/static/app.js b/static/app.js index 3ac307d..9d484a7 100644 --- a/static/app.js +++ b/static/app.js @@ -1,5 +1,12 @@ 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 { constructor() { @@ -23,21 +30,11 @@ export class CanvasApp { this.dbGroup = document.getElementById('db-group'); this.cacheGroup = document.getElementById('cache-group'); this.selectedNode = null; - this.computeTypes = ['webserver', 'microservice']; 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(); } @@ -56,10 +53,12 @@ export class CanvasApp { } }); this.sidebar.addEventListener('dragstart', (e) => { - if (e.target.classList.contains('component-icon')) { - e.dataTransfer.setData('text/plain', e.target.getAttribute('data-type')); - e.target.classList.add('dragging'); - } + 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')) { @@ -69,26 +68,27 @@ export class CanvasApp { this.canvasContainer.addEventListener('dragover', (e) => e.preventDefault()); this.canvasContainer.addEventListener('drop', (e) => { - e.preventDefault(); 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 node = new ComponentNode(type, x, y, this); + + const props = generateDefaultProps(plugin); + const node = new ComponentNode(type, x, y, this, props); node.x = x; node.y = y; - if (this.placeholderText) { - this.placeholderText.remove(); - this.placeholderText = null; - } }); this.runButton.addEventListener('click', () => { const designData = this.exportDesign(); - console.log(JSON.stringify(designData)); + console.log(JSON.stringify(designData)) }); this.canvas.addEventListener('click', () => { @@ -102,21 +102,35 @@ export class CanvasApp { this.propsSaveBtn.addEventListener('click', () => { if (!this.activeNode) return; - const nodeObj = this.activeNode; + + const node = this.activeNode; const panel = this.nodePropsPanel; - const newLabel = panel.querySelector("input[name='label']").value; - nodeObj.updateLabel(newLabel); - if (nodeObj.type === 'Database') { - nodeObj.props.replication = parseInt(panel.querySelector("input[name='replication']").value, 10); - } - 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; + const plugin = PluginRegistry.get(node.type); + + if (!plugin || !plugin.props) { + this.hidePropsPanel(); + return; } - 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(); }); @@ -147,54 +161,47 @@ export class CanvasApp { }); } - showPropsPanel(nodeObj) { - this.activeNode = nodeObj; + showPropsPanel(node) { + this.activeNode = node; + const plugin = PluginRegistry.get(node.type); const panel = this.nodePropsPanel; - if (nodeObj.type === 'user') { + if (!plugin || this.arrowMode) { this.hidePropsPanel(); return; } - // Position the panel (optional, or you can use fixed top/right) - const bbox = nodeObj.group.getBBox(); - const ctm = nodeObj.group.getCTM(); + 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'; - // Always show label group - this.labelGroup.style.display = 'block'; - panel.querySelector("input[name='label']").value = nodeObj.props.label; + // Hide all groups first + const allGroups = panel.querySelectorAll('.prop-group, #label-group, #compute-group, #lb-group'); + allGroups.forEach(g => g.style.display = 'none'); - // Show DB fields if it's a Database - this.dbGroup.style.display = nodeObj.type === 'Database' ? 'block' : 'none'; - if (nodeObj.type === 'Database') { - this.dbGroup.querySelector("input[name='replication']").value = nodeObj.props.replication; - } + 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); + } - // Show cache fields if it's a cache - const isCache = nodeObj.type === 'cache'; - this.cacheGroup.style.display = isCache ? 'block' : 'none'; - 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'; + // Set value + if (input) { + input.value = node.props[prop.name] ?? prop.default; + } } this.propsSaveBtn.disabled = false; 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() { @@ -228,35 +235,19 @@ export class CanvasApp { const nodes = this.placedComponents .filter(n => n.type !== 'user') .map(n => { - const baseProps = { label: n.props.label }; - - // 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 { + const plugin = PluginRegistry.get(n.type); + const result = { id: n.id, type: n.type, - props: baseProps, - position: { - x: n.x, - y: n.y - } + 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 => ({ diff --git a/static/game.html b/static/game.html index 034e76a..271c27d 100644 --- a/static/game.html +++ b/static/game.html @@ -604,55 +604,45 @@