commit d039c391e71e86f0896f536c6b11e3c217b54be2 Author: Thomas Lovén Date: Fri Aug 21 08:41:38 2020 +0200 Initial commit. POC diff --git a/.giattributes b/.giattributes new file mode 100644 index 0000000..1a6bd45 --- /dev/null +++ b/.giattributes @@ -0,0 +1 @@ +package-lock.json binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f614508 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +index.js diff --git a/example.html b/example.html new file mode 100644 index 0000000..7d2d506 --- /dev/null +++ b/example.html @@ -0,0 +1,48 @@ + + + + Script graph demo + + + + + +
+
+
+
+ + +
+
+ + + +
+
+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5201ab6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,138 @@ +{ + "name": "script-graph", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@mdi/js": { + "version": "5.5.55", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-5.5.55.tgz", + "integrity": "sha512-vbw1QW3M9A4vObU9WmTETTG7n7feC9HSn/3up8ZYk/M3K9fGL9FPtw0+bdASRu1UOBgEsqC9eBhNW10IOcwMIg==" + }, + "@rollup/plugin-node-resolve": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz", + "integrity": "sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.17.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.0.tgz", + "integrity": "sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@vanillawc/wc-codemirror": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/@vanillawc/wc-codemirror/-/wc-codemirror-1.8.10.tgz", + "integrity": "sha512-UKMD/UOpF1uRl29nlwvwQqSMBqsl+uDWYlGx82wIYbIBJkeqPrSo1Ez1rGi9jc1CL7/XwUr7u+l/kTwZAEkrEg==" + }, + "builtin-modules": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "lit-element": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.3.1.tgz", + "integrity": "sha512-tOcUAmeO3BzwiQ7FGWdsshNvC0HVHcTFYw/TLIImmKwXYoV0E7zCBASa8IJ7DiP4cen/Yoj454gS0qqTnIGsFA==", + "requires": { + "lit-html": "^1.1.1" + } + }, + "lit-html": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.2.1.tgz", + "integrity": "sha512-GSJHHXMGLZDzTRq59IUfL9FCdAlGfqNp/dEa7k7aBaaWD+JKaCjsAk9KYm2V12ItonVaYx2dprN66Zdm1AuBTQ==" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.26.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.4.tgz", + "integrity": "sha512-6+qsGuP0MXGd7vlYmk72utm1MrgZj5GfXibGL+cRkKQ9+ZL/BnFThDl0D5bcl7AqlzMjAQXRAwZX1HVm22M/4Q==", + "dev": true, + "requires": { + "fsevents": "~2.1.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..644b461 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "script-graph", + "private": true, + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@rollup/plugin-node-resolve": "^9.0.0", + "rollup": "^2.26.4" + }, + "dependencies": { + "@mdi/js": "^5.5.55", + "@vanillawc/wc-codemirror": "^1.8.10", + "lit-element": "^2.3.1" + } +} diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..a5e6a53 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,12 @@ +import resolve from "@rollup/plugin-node-resolve"; + +export default { + input: "./src/main.js", + output: [ + { + file: "index.js", + format: "cjs" + } + ], + plugins: [resolve()] +}; diff --git a/src/demo-config.js b/src/demo-config.js new file mode 100644 index 0000000..0005133 --- /dev/null +++ b/src/demo-config.js @@ -0,0 +1,63 @@ +export const demoConfig = [ + { service: "light.turn_on", + data: { + entity_id: "group.bedroom", + brightness: 100, + }, + }, + { condition: "state", + entity_id: "device_tracker.paulus", + state: "home", + }, + { delay: "01:00", + }, + { wait_template: "{{ is_state(media_player.floor', 'stop') }}", + }, + { event: "LOGBOOK_ENTRY", + event_data: { + name: "Paulus", + message: "is waking up", + entity_id: "device_tracker.paulus", + domain: "light", + }, + }, + { repeat: { + count: "5", + sequence: [ + { delay: 2 + }, + { service: "light.toggle", + data: { + entity_id: "light.bed_light", + }, + }, + ], + }, + }, + { choose: [ + { conditions: [], + sequence: [ + { service: "test"}, + { service: "test"}, + { service: "test"}, + ], + }, + { conditions: [], + sequence: [ + { service: "test"}, + ], + }, + { conditions: [], + sequence: [ + { service: "test"}, + { service: "test"}, + ], + }, + ], + default: [ + { service: "test"}, + { service: "test"}, + { service: "test"}, + ], + }, +]; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..b6c0a7d --- /dev/null +++ b/src/main.js @@ -0,0 +1,366 @@ +import {demoConfig} from "./demo-config"; +import "@vanillawc/wc-codemirror"; + +import { + LitElement, + html, + css, + svg, + property +} from "lit-element"; + +import { + mdiCallSplit, + mdiAbTesting, + mdiCheck, + mdiClose, + mdiChevronRight, + mdiExclamation, + mdiTimerOutline, + mdiTrafficLight, + mdiRefresh, + mdiArrowUp, + mdiCodeJson, + mdiCheckBoxOutline, + mdiCheckboxBlankOutline, + mdiAsterisk, +} from "@mdi/js"; + + +const SIZE = 35; +const BORDER = 5; +const R = SIZE/2; +const DIST = 20; + +const ICONS = { + choose: mdiCallSplit, + chooseChoice: mdiCheckBoxOutline, + chooseDefault: mdiCheckboxBlankOutline, + condition: mdiAbTesting, + TRUE: mdiCheck, + FALSE: mdiClose, + service: mdiChevronRight, + event: mdiExclamation, + delay: mdiTimerOutline, + wait: mdiTrafficLight, + loop: mdiRefresh, + loopReturn: mdiArrowUp, + YAML: mdiCodeJson, +}; + +class ScriptGraph extends LitElement { + + static get properties() { + return { + tree: {}, + selected: {}, + }; + } + + _select(idx) { + this.selected = idx; + const ev = new CustomEvent("selected", {detail: idx}); + this.dispatchEvent(ev); + } + + _draw_node(x, y, node, idx) { + const selected = (Array.isArray(this.selected) + ? (this.selected[0] === idx) + : false + ) || (idx === this.selected); + return svg` + this._select(idx)} + /> + + ${node in ICONS + ? svg` + + + + ` + : svg` + ${node} + `} + + `; + } + _draw_connector(x1, y1, x2, y2) { + return svg` + + `; + } + + + draw_tree(tree) { + if(!tree) return {svg: svg``, width: 1, height: 1}; + let nodes = []; + let connections = []; + if(!Array.isArray(tree)) { + const selected = tree.idx == this.selected; + if(tree.type === "loop") { + const seq = this.draw_tree(tree.sequence); + const sep = seq.width + DIST; + const left = -(seq.width + SIZE + DIST)/2 + SIZE/2; + const right = left+DIST+seq.width/2 + SIZE/2; + return { + svg: svg` + ${this._draw_connector( + 0, R, + left, SIZE + DIST + R + )} + ${this._draw_connector( + 0, R, + right, SIZE + DIST + R + )} + ${this._draw_connector( + right, SIZE + DIST + seq.height - R, + 0, SIZE + DIST + seq.height + DIST + )} + ${this._draw_connector( + left, SIZE + DIST + seq.height - R, + 0, SIZE + DIST + seq.height + DIST + )} + ${this._draw_connector( + left, SIZE + DIST + R, + left, SIZE + DIST + seq.height - R + )} + ${this._draw_node(0, 0, "loop", tree.idx)} + ${this._draw_node(left, SIZE+DIST+seq.height/2-R, "loopReturn", tree.idx)} + + ${seq.svg} + + `, + height: SIZE + DIST + seq.height + DIST, + width: seq.width + SIZE, + } + } + if(tree.type === "condition") { + const sep = SIZE + DIST; + return { + svg: svg` + ${this._draw_connector( + 0, R, + -sep/2, SIZE + DIST + R + )} + ${this._draw_connector( + 0, R, + sep/2, SIZE + DIST + R + )} + ${this._draw_connector( + -sep/2, SIZE + DIST + R, + 0, SIZE + DIST + SIZE + DIST, + )} + ${this._draw_node(0, 0, "condition", tree.idx)} + ${this._draw_node(-sep/2, SIZE+DIST, "TRUE", tree.idx)} + ${this._draw_node(sep/2, SIZE+DIST, "FALSE", tree.idx)} + `, + height: SIZE + DIST + SIZE + DIST, + width: SIZE + DIST + SIZE, + } + } + + if(tree.type === "choose") { + const choices = tree.choices.map(x => this.draw_tree(x)); + const maxHeight = choices.reduce((a,i) => Math.max(a, i.height), 0); + const sep = SIZE + DIST; + const totWidth = choices.reduce((a,i) => a+i.width, 0)+DIST*(choices.length-1); + const offset = choices.map((sum => value => sum += value.width)(0)); + return { + svg: svg` + ${choices.map((choice, idx) => this._draw_connector( + 0, + R, + -totWidth/2+idx*DIST + (idx?offset[idx-1]:0) + choice.width/2, + SIZE + DIST + R, + ))} + ${choices.map((choice, idx) => this._draw_connector( + -totWidth/2+idx*DIST + (idx?offset[idx-1]:0)+ choice.width/2, + SIZE + DIST + R, + -totWidth/2+idx*DIST + (idx?offset[idx-1]:0)+ choice.width/2, + SIZE + DIST + R + DIST, + ))} + ${choices.map((choice, idx) => svg` + ${this._draw_connector( + -totWidth/2+idx*DIST + (idx?offset[idx-1]:0) + choice.width/2, + SIZE + DIST + R + DIST + choice.height, + -totWidth/2+idx*DIST + (idx?offset[idx-1]:0) + choice.width/2, + SIZE + DIST + R + DIST + maxHeight + DIST + )} + ${this._draw_connector( + -totWidth/2+idx*DIST + (idx?offset[idx-1]:0) + choice.width/2, + SIZE + DIST + R + DIST + maxHeight + DIST, + 0, + SIZE + DIST + R + DIST + maxHeight + DIST + SIZE + DIST + )} + `)} + ${this._draw_node(0, 0, tree.type, tree.idx)} + ${choices.map((choice, idx) => svg` + + ${choice.svg} + + `)} + `, + height: SIZE + DIST + R + DIST + maxHeight + DIST + SIZE + DIST, + width: totWidth, + } + } + + return { + svg: this._draw_node(0, 0, tree.type, tree.idx), + height: SIZE, + width: SIZE, + } + } + let height = 0; + let width = 0; + for (const [idx, node] of tree.entries()) { + const n = this.draw_tree(node); + if(idx) { + nodes.splice(nodes.length-1, 0, this._draw_connector( + 0, + height-DIST, + 0, + height+DIST + )); + } + nodes.push(svg`${n.svg}`); + height += n.height+DIST; + width = Math.max(width, n.width); + } + height = height-DIST; + return { + svg: svg`${nodes.map((node, idx) => node)}`, + height, + width, + }; + + } + + render() { + let processed_tree = this.draw_tree(this.tree); + return html` + + + + ${processed_tree.svg} + + + `; + } + +} + +customElements.define("script-graph", ScriptGraph); + +let index_counter = 0; +let nodes = []; + +const structure_tree = (inp) => { + if(!inp) return null; + if(Array.isArray(inp)) return inp.map(structure_tree); + let data = {}; + let type = "YAML"; + + const idx = index_counter++; + if("service" in inp) type = "service"; + if("condition" in inp) type = "condition"; + if("delay" in inp) type = "delay"; + if("wait_template" in inp) type = "wait"; + if("event" in inp) type = "event"; + if("repeat" in inp) { + type = "loop"; + data = {sequence: structure_tree(inp.repeat.sequence)}; + } + if("choose" in inp) { + type = "choose"; + let choices = []; + for (const [i,c] of inp.choose.entries()) { + const header = { + type: "chooseChoice", + idx: [idx, i], + } + choices.push([header].concat(structure_tree(c.sequence))); + } + choices.push([{ + type: "chooseDefault", + idx: [idx, -1], + }].concat(structure_tree(inp.default))); + data = { + choices, + } + } + nodes[idx] = inp; + return {type, + idx, + ...data}; + +} + +window.onload = () => { + + let src = demoConfig; + + const fullcode = document.querySelector("wc-codemirror"); + fullcode.mode = "yaml"; + window.setTimeout(()=> fullcode.value = jsyaml.safeDump(src), 100); + + const updateButton = document.querySelector("#updateButton"); + updateButton.addEventListener('click', () => { + src = jsyaml.safeLoad(fullcode.value); + index_counter = 0; + nodes = []; + graph.tree = structure_tree(src); + }); + + const graph = document.createElement("script-graph"); + graph.tree = structure_tree(src); + graph.addEventListener("selected", (ev) => { + const idx = ev.detail; + const code = document.querySelector("code"); + let c; + if(Array.isArray(idx)) { + c = nodes[idx[0]].choose[idx[1]]; + } else { + c = nodes[idx]; + } + code.innerHTML = JSON.stringify(c, null, ' '); + + }) + document.querySelector("#graph").appendChild(graph); + + +}