use xyflow internally instead of svelvet for graph rendering (#4173)

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): fix while loops + theme

* fix(frontend): wip

* fix(frontend): wip

* fix(frontend): same view as before

* fix(frontend): wip

* fix(frontend): wip

* feat(frontend): flow status viewer

* fix(frontend): wip

* feat(frontend): migrate to xyflow

* feat(frontend): dataflow

* feat(frontend): clean up

* feat(frontend): clean up

* feat(frontend): clean up

* feat(frontend): fix min-height

* feat(frontend): clean up

* feat(frontend): add missing insert button for empty loops

* feat(frontend): Fix insert

* feat(frontend): Fix branch one

* feat(frontend): Support default branch for branch one

* feat(frontend): fix add and delete branches

* feat(frontend): Make the iterations menu work

* feat(frontend): Fix graph height in the flow status viewer

* feat(frontend): code improvement

* feat(frontend): add missing trigger

* feat(frontend): add support for triggers

* feat(frontend): fix move

* feat(frontend): fix No branch for branch one

* feat(frontend): fix light theme

* feat(frontend): fix build

* feat(frontend): remove dead code

* feat(frontend): fix move

* feat(frontend): fix vertical alignement

* feat(frontend): fix viewport

* feat(frontend): style fix

* feat(frontend): fix viewgraph

* feat(frontend): fix build

* feat(frontend): Fix node position

* feat(frontend): UI nits

* feat(frontend): UI nits

* feat(frontend): fix zoom level + fix insert button position

* feat(frontend): svelvet clean up

* feat(frontend): fix paste button position

* feat(frontend): clean up

* feat(frontend): migrate Decision Tree

* feat(frontend): clean up

* feat(frontend): add callback

* feat(frontend): migrate Decision Tree

* feat(frontend): fix build

* feat(frontend): fix branches

* feat(frontend): fix branches

* feat(frontend): fix insert

* feat(frontend): wip

* feat(frontend): done

* feat(frontend): done

* feat(frontend): fix reactivity

* feat(frontend): fix z-index issues

* feat(frontend): fix bg issue

* feat(frontend): fix border status

* add padding to flow height

* Update FlowGraphV2.svelte

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Faton Ramadani
2024-08-19 09:17:56 +02:00
committed by GitHub
parent de4f5e3015
commit 632bd18a2e
70 changed files with 2097 additions and 4405 deletions
+63 -63
View File
@@ -16,6 +16,7 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.9.9",
"@windmill-labs/svelte-dnd-action": "latest",
"@xyflow/svelte": "^0.1.14",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.0.0",
@@ -1778,6 +1779,14 @@
"resolved": "https://registry.npmjs.org/@streamparser/json/-/json-0.0.20.tgz",
"integrity": "sha512-VqAAkydywPpkw63WQhPVKCD3SdwXuihCUVZbbiY3SfSTGQyHmwRoq27y4dmJdZuJwd5JIlQoMPyGvMbUPY0RKQ=="
},
"node_modules/@svelte-put/shortcut": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz",
"integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==",
"peerDependencies": {
"svelte": "^3.55.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@sveltejs/adapter-static": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.1.tgz",
@@ -2023,8 +2032,7 @@
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"dev": true
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-contour": {
"version": "3.0.6",
@@ -2052,7 +2060,6 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
@@ -2109,7 +2116,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dev": true,
"dependencies": {
"@types/d3-color": "*"
}
@@ -2156,8 +2162,7 @@
"node_modules/@types/d3-selection": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz",
"integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==",
"dev": true
"integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg=="
},
"node_modules/@types/d3-shape": {
"version": "3.1.5",
@@ -2190,7 +2195,6 @@
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz",
"integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==",
"dev": true,
"dependencies": {
"@types/d3-selection": "*"
}
@@ -2199,7 +2203,6 @@
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dev": true,
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
@@ -2291,8 +2294,7 @@
"node_modules/@types/pug": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.9.tgz",
"integrity": "sha512-Yg4LkgFYvn1faISbDNWmcAC1XoDT8IoMUFspp5mnagKk+UvD2N0IWt5A7GRdMubsNWqgCLmrkf8rXkzNqb4szA==",
"dev": true
"integrity": "sha512-Yg4LkgFYvn1faISbDNWmcAC1XoDT8IoMUFspp5mnagKk+UvD2N0IWt5A7GRdMubsNWqgCLmrkf8rXkzNqb4szA=="
},
"node_modules/@types/semver": {
"version": "7.5.5",
@@ -2512,6 +2514,34 @@
"svelte": ">=3.23.0 || ^5.0.0-next.0"
}
},
"node_modules/@xyflow/svelte": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.14.tgz",
"integrity": "sha512-DdlFz+JKW1sQt8Q8O1DoSsroTvFk1RzmpcEZcGARt1REbnLy4BYG/03piaiclaC4iHK6feaPDvxpF057CkMCTw==",
"dependencies": {
"@svelte-put/shortcut": "^3.1.0",
"@xyflow/system": "0.0.37",
"classcat": "^5.0.4",
"svelte-preprocess": "^5.1.3"
},
"peerDependencies": {
"svelte": "^3.0.0 || ^4.0.0"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.37",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.37.tgz",
"integrity": "sha512-hSIhezhxgftPUpC+xiQVIorcRILZUOWlLjpYPTyGWRu8s4RJvM4GqvrsFmD5OnMKXLgpU7/PqqUibDVO67oWQQ==",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/@zerodevx/svelte-toast": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@zerodevx/svelte-toast/-/svelte-toast-0.9.5.tgz",
@@ -2974,7 +3004,6 @@
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": {
"node": "*"
}
@@ -3242,6 +3271,11 @@
"consola": "^3.2.3"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -3309,8 +3343,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"devOptional": true
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/confbox": {
"version": "0.1.6",
@@ -3922,7 +3955,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz",
"integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -4149,8 +4181,7 @@
"node_modules/es6-promise": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==",
"dev": true
"integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="
},
"node_modules/esbuild": {
"version": "0.21.5",
@@ -4844,8 +4875,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
@@ -4963,7 +4993,6 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"devOptional": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -4995,7 +5024,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"devOptional": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5005,7 +5033,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"devOptional": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -5122,8 +5149,7 @@
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/graphemer": {
"version": "1.4.0",
@@ -5431,7 +5457,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"devOptional": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -5440,8 +5465,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"devOptional": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "1.3.8",
@@ -5921,7 +5945,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=10"
}
@@ -7000,7 +7024,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -7089,7 +7112,6 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -7202,7 +7224,7 @@
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "github",
@@ -7511,7 +7533,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"devOptional": true,
"dependencies": {
"wrappy": "1"
}
@@ -7708,7 +7729,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7796,8 +7816,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true,
"license": "ISC"
"devOptional": true
},
"node_modules/picomatch-browser": {
"version": "2.2.6",
@@ -7887,7 +7906,7 @@
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -7902,7 +7921,6 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
@@ -8063,7 +8081,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz",
"integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==",
"dev": true,
"devOptional": true,
"dependencies": {
"lilconfig": "^2.0.5",
"yaml": "^2.1.1"
@@ -9030,7 +9048,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz",
"integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==",
"dev": true,
"dependencies": {
"es6-promise": "^3.1.2",
"graceful-fs": "^4.1.3",
@@ -9042,7 +9059,6 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -9212,7 +9228,6 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^0.2.5",
@@ -9867,27 +9882,26 @@
}
},
"node_modules/svelte-preprocess": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.0.tgz",
"integrity": "sha512-EkErPiDzHAc0k2MF5m6vBNmRUh338h2myhinUw/xaqsLs7/ZvsgREiLGj03VrSzbY/TB5ZXgBOsKraFee5yceA==",
"dev": true,
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz",
"integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==",
"hasInstallScript": true,
"dependencies": {
"@types/pug": "^2.0.6",
"detect-indent": "^6.1.0",
"magic-string": "^0.27.0",
"magic-string": "^0.30.5",
"sorcery": "^0.11.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">= 14.10.0"
"node": ">= 16.0.0"
},
"peerDependencies": {
"@babel/core": "^7.10.2",
"coffeescript": "^2.5.1",
"less": "^3.11.3 || ^4.0.0",
"postcss": "^7 || ^8",
"postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0",
"postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
"pug": "^3.0.0",
"sass": "^1.26.8",
"stylus": "^0.55.0",
@@ -9928,23 +9942,10 @@
}
}
},
"node_modules/svelte-preprocess/node_modules/magic-string": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
"integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13"
},
"engines": {
"node": ">=12"
}
},
"node_modules/svelte-preprocess/node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"dependencies": {
"min-indent": "^1.0.0"
},
@@ -10324,7 +10325,7 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -10787,8 +10788,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"devOptional": true
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/write-file-atomic": {
"version": "5.0.1",
+1
View File
@@ -97,6 +97,7 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.9.9",
"@windmill-labs/svelte-dnd-action": "latest",
"@xyflow/svelte": "^0.1.14",
"ag-charts-community": "^9.0.1",
"ag-charts-enterprise": "^9.0.1",
"ag-grid-community": "^31.0.0",
@@ -1,12 +1,11 @@
<script lang="ts">
import { FlowGraph } from './graph'
import type { FlowModule, FlowValue } from '$lib/gen'
import { createEventDispatcher } from 'svelte'
import { twMerge } from 'tailwind-merge'
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
export let flow: {
summary: string
@@ -30,7 +29,7 @@
class="{noSide ? 'col-span-3' : 'sm:col-span-2 col-span-3'} w-full border max-h-full"
class:overflow-auto={overflowAuto}
>
<FlowGraph
<FlowGraphV2
{download}
minHeight={400}
modules={flow?.value?.modules}
@@ -39,7 +38,6 @@
stepDetail = e.detail
dispatch('select', stepDetail)
}}
rebuildOnChange={flow}
/>
</div>
{/if}
@@ -16,12 +16,7 @@
import { Badge, Button, Tab } from './common'
import DisplayResult from './DisplayResult.svelte'
import Tabs from './common/tabs/Tabs.svelte'
import {
FlowGraph,
type DurationStatus,
type FlowStatusViewerContext,
type GraphModuleState
} from './graph'
import { type DurationStatus, type FlowStatusViewerContext, type GraphModuleState } from './graph'
import ModuleStatus from './ModuleStatus.svelte'
import { emptyString, msToSec, truncateRev } from '$lib/utils'
import JobArgs from './JobArgs.svelte'
@@ -33,6 +28,7 @@
import { writable, type Writable } from 'svelte/store'
import Alert from './common/alert/Alert.svelte'
import FlowGraphViewerStep from './FlowGraphViewerStep.svelte'
import FlowGraphV2 from './graph/FlowGraphV2.svelte'
const dispatch = createEventDispatcher()
@@ -594,6 +590,7 @@
let stepDetail: FlowModule | string | undefined = undefined
let storedListJobs: Record<number, Job> = {}
let wrapperHeight: number = 0
</script>
{#if notAnonynmous}
@@ -905,7 +902,7 @@
{#if render}
{#if job.raw_flow && !isListJob}
<div class="{selected != 'graph' ? 'hidden' : ''} grow mt-4">
<div class="grid grid-cols-3 border h-full">
<div class="grid grid-cols-3 border h-full" bind:clientHeight={wrapperHeight}>
<div class="col-span-2 bg-surface-secondary">
<div class="flex flex-col">
{#each Object.values($retryStatus) as count}
@@ -924,8 +921,9 @@
{/each}
</div>
<FlowGraph
<FlowGraphV2
download
minHeight={wrapperHeight}
success={jobId != undefined && isSuccess(job?.['success'])}
flowModuleStates={$localModuleStates}
on:select={(e) => {
@@ -950,6 +948,7 @@
}}
on:selectedIteration={(e) => {
let detail = e.detail
setModuleState(detail.moduleId, {
selectedForloop: detail.id,
selectedForloopIndex: detail.index
@@ -957,7 +956,6 @@
globalRefreshes[detail.moduleId]?.({ job: detail.id, index: detail.index })
}}
modules={job.raw_flow?.modules ?? []}
failureModule={job.raw_flow?.failure_module}
/>
</div>
<div
@@ -46,7 +46,6 @@
<DecisionTreePreview
bind:nodes
bind:component
{rebuildOnChange}
{paneHeight}
{paneWidth}
on:render={() => {
@@ -1,65 +1,81 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { DecisionTreeNode } from '../component'
import { twMerge } from 'tailwind-merge'
import InsertDecisionTreeNode from './decisionTree/InsertDecisionTreeNode.svelte'
import { X } from 'lucide-svelte'
import NodeWrapper from '$lib/components/graph/renderers/nodes/NodeWrapper.svelte'
export let node: DecisionTreeNode
export let editable: boolean = true
export let canDelete: boolean = true
export let label: string = ''
export let data: {
node: DecisionTreeNode
canDelete: boolean
nodeCallbackHandler: (
event: string,
detail: string,
graphNode: DecisionTreeNode | undefined,
parentIds: string[],
branchInsert: boolean
) => void
parentIds: string[]
branchHeader: boolean
}
let open: boolean = false
const dispatch = createEventDispatcher<{
nodeInsert: void
delete: void
addBranch: void
removeBranch: void
}>()
</script>
<div class="relative h-full">
<div
class={twMerge(
'w-full h-full border border-gray-400',
'flex flex-row gap-2 items-center justify-between rounded-sm overflow-hidden'
)}
style="min-width: 275px; max-height: 80px; background-color: {document.documentElement.classList.contains(
'dark'
)
? '#2e3440'
: '#dfe6ee'}"
>
<div class="ml-4 text-xs font-normal text-primary"> {label} </div>
</div>
{#if canDelete}
<div class="w-[27px] absolute -top-[32px] left-[50%] right-[50%] -translate-x-1/2">
<button
title="Delete branch"
on:click|preventDefault|stopPropagation={() => dispatch('removeBranch')}
type="button"
class="text-primary bg-surface border mx-[1px] border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<X class="m-[5px]" size={15} />
</button>
</div>
{/if}
{#if node.id !== 'end' && editable}
<NodeWrapper>
<div class="relative h-full">
<div
class={twMerge(
'absolute -bottom-10 left-1/2 transform -translate-x-1/2 flex items-center',
open ? 'z-20' : ''
'w-full h-full border border-gray-400',
'flex flex-row gap-2 items-center justify-between rounded-sm overflow-hidden'
)}
style="width: 275px; height: 34px; background-color: {document.documentElement.classList.contains(
'dark'
)
? '#2e3440'
: '#dfe6ee'}"
>
<InsertDecisionTreeNode
on:node={() => dispatch('nodeInsert')}
on:addBranch={() => dispatch('addBranch')}
canAddBranch={false}
/>
<div class="ml-4 text-xs font-normal text-primary"> {data.node.label} </div>
</div>
{/if}
</div>
{#if data.canDelete}
<div class="w-[27px] absolute -top-[32px] left-[50%] right-[50%] -translate-x-1/2">
<button
title="Delete branch"
on:click|preventDefault|stopPropagation={() => {
data.nodeCallbackHandler('removeBranch', data.node.id, data.node, data.parentIds, false)
}}
type="button"
class="text-primary bg-surface border mx-[1px] border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<X class="m-[5px]" size={15} />
</button>
</div>
{/if}
{#if data.node.id !== 'end'}
<div
class={twMerge(
'absolute -bottom-10 left-1/2 transform -translate-x-1/2 flex items-center',
open ? 'z-20' : ''
)}
>
<InsertDecisionTreeNode
on:node={() => {
data.nodeCallbackHandler(
'nodeInsert',
data.node.id,
data.node,
data.parentIds ?? [],
data.branchHeader
)
}}
on:addBranch={() => {
data.nodeCallbackHandler('addBranch', data.node.id, data.node, data.parentIds, true)
}}
canAddBranch={false}
/>
</div>
{/if}
</div>
</NodeWrapper>
@@ -1,5 +1,5 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'
import { getContext } from 'svelte'
import type { DecisionTreeNode } from '../component'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
@@ -9,80 +9,96 @@
import { X } from 'lucide-svelte'
import { getStateColor } from '$lib/components/graph'
import Tooltip from '$lib/components/Tooltip.svelte'
import NodeWrapper from '$lib/components/graph/renderers/nodes/NodeWrapper.svelte'
export let data: {
node: DecisionTreeNode
canDelete: boolean
canAddBranch: boolean
selected: boolean
index: number
nodeCallbackHandler: (
event: string,
detail: string,
graphNode: DecisionTreeNode | undefined,
parentIds: string[],
branchInsert: boolean
) => void
parentIds: string[]
}
export let node: DecisionTreeNode
export let selected = false
export let canDelete: boolean = true
export let canAddBranch: boolean = true
export let index: number
export let darkMode: boolean
let open: boolean = false
const dispatch = createEventDispatcher<{
select: string
nodeInsert: void
delete: void
addBranch: void
}>()
const { selectedNodeId } = getContext<{
selectedNodeId: Writable<string | undefined>
}>('DecisionTreeEditor')
$: selected = $selectedNodeId === node.id
$: selected = $selectedNodeId === data.node.id
</script>
<div class="relative rounded-sm group">
<Button
class={twMerge(
'p-2 bg-surface w-full h-8 relative rounded-sm border border-gray-400',
selected ? 'outline outline-2 outline-offset-2 outline-gray-600' : '',
'flex flex-row gap-2 items-center justify-between'
)}
style="width: 275px; height: 34px; background-color: {getStateColor(undefined, darkMode)};"
on:click={() => {
selected = true
dispatch('select', node.id)
}}
>
<div class="ml-2 text-xs font-normal text-primary truncate">
{node.label === '' ? `Tab: ${node.id}` : node.label}
</div>
<Badge color="indigo" small>
Tab index: {index}
<Tooltip>
You can manually select a node using the <b>setTab</b> function with this index in a frontend
runnable.
</Tooltip>
</Badge>
</Button>
{#if canDelete}
<button
<NodeWrapper let:darkMode>
<div class="rounded-sm group">
<Button
class={twMerge(
'absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-primary',
'border-[1.5px] border-gray-700 bg-surface duration-150 hover:bg-red-400 hover:text-white hover:border-red-700',
'group-hover:opacity-100 opacity-0'
'p-2 bg-surface w-full h-8 relative rounded-sm border border-gray-400',
selected ? 'outline outline-2 outline-offset-2 outline-gray-600' : '',
'flex flex-row gap-2 items-center justify-between'
)}
on:click|preventDefault|stopPropagation={() => dispatch('delete')}
style="width: 275px; height: 34px; background-color: {getStateColor(
undefined,
darkMode,
'#fff'
)};"
on:click={() => {
selected = true
data.nodeCallbackHandler('select', data.node.id, data.node, data.parentIds, false)
}}
>
<X class="mx-[3px]" size={14} strokeWidth={2} />
</button>
{/if}
<div class="ml-2 text-xs font-normal text-primary truncate">
{data.node.label === '' ? `Tab: ${data.node.id}` : data.node.label}
</div>
<Badge color="indigo" small>
Tab index: {data.index}
<Tooltip>
You can manually select a node using the <b>setTab</b> function with this index in a frontend
runnable.
</Tooltip>
</Badge>
</Button>
{#if node.id !== 'end'}
<div
class={twMerge(
'absolute -bottom-10 left-1/2 transform -translate-x-1/2 flex items-center',
open ? 'z-20' : ''
)}
>
<InsertDecisionTreeNode
on:node={() => dispatch('nodeInsert')}
on:addBranch={() => dispatch('addBranch')}
canAddBranch={canAddBranch || node.next.length > 1}
canAddNode={node.next.length <= 1}
/>
</div>
{/if}
</div>
{#if data.canDelete}
<button
class={twMerge(
'absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-primary',
'border-[1.5px] border-gray-700 bg-surface duration-150 hover:bg-red-400 hover:text-white hover:border-red-700',
'group-hover:opacity-100 opacity-0'
)}
on:click|preventDefault|stopPropagation={() => {
data.nodeCallbackHandler('delete', data.node.id, data.node, data.parentIds, false)
}}
>
<X class="mx-[3px]" size={14} strokeWidth={2} />
</button>
{/if}
{#if data.node.id !== 'end'}
<div
class={twMerge(
'absolute -bottom-10 left-1/2 transform -translate-x-1/2 flex items-center',
open ? 'z-20' : ''
)}
>
<InsertDecisionTreeNode
on:node={() => {
data.nodeCallbackHandler('nodeInsert', data.node.id, data.node, data.parentIds, false)
}}
on:addBranch={() => {
data.nodeCallbackHandler('addBranch', data.node.id, data.node, data.parentIds, true)
}}
canAddBranch={data.canAddBranch || data.node.next.length > 1}
canAddNode={data.node.next.length <= 1}
/>
</div>
{/if}
</div>
</NodeWrapper>
@@ -1,16 +1,15 @@
<script lang="ts">
import Svelvet from '$lib/components/graph/svelvet/container/views/Svelvet.svelte'
import type { UserEdgeType } from '$lib/components/graph/svelvet/types'
import { NODE, type Node } from '$lib/components/graph'
import { createEventDispatcher, getContext, onMount } from 'svelte'
import { NODE } from '$lib/components/graph'
import { sugiyama, dagStratify, decrossOpt, coordCenter } from 'd3-dag'
import { deepEqual } from 'fast-equals'
import {
SvelteFlow,
type Node,
type Edge,
ConnectionLineType,
type Viewport,
Controls
} from '@xyflow/svelte'
import DecisionTreeGraphNode from '../DecisionTreeGraphNode.svelte'
import DecisionTreeGraphHeader from '../DecisionTreeGraphHeader.svelte'
import type { Writable } from 'svelte/store'
import type { AppComponent, DecisionTreeNode } from '../../component'
import {
addNewBranch,
addNode,
@@ -20,22 +19,23 @@
removeBranch,
removeNode
} from './utils'
import { createEdge, createNode } from './nodeHelpers'
import DecisionTreeGraphNode from '../DecisionTreeGraphNode.svelte'
import DecisionTreeGraphHeader from '../DecisionTreeGraphHeader.svelte'
import { writable, type Writable } from 'svelte/store'
import type { AppComponent, DecisionTreeNode } from '../../component'
import { createEventDispatcher, getContext } from 'svelte'
import type { AppViewerContext } from '$lib/components/apps/types'
import { deleteGridItem } from '../../appUtils'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
export let nodes: DecisionTreeNode[]
export let rebuildOnChange: any = undefined
export let paneWidth = 0
export let paneHeight = 0
export let component: AppComponent
let displayedNodes: Node[] = []
let edges: UserEdgeType[] = []
let scroll = false
let darkMode = false
const nodesStore = writable<Node[]>([])
const edgesStore = writable<Edge[]>([])
const dispatch = createEventDispatcher()
@@ -43,109 +43,9 @@
selectedNodeId: Writable<string | undefined>
}>('DecisionTreeEditor')
$: rebuildOnChange && triggerRebuild()
let oldRebuildOnChange = rebuildOnChange ? JSON.parse(JSON.stringify(rebuildOnChange)) : undefined
function triggerRebuild() {
if (!deepEqual(oldRebuildOnChange, rebuildOnChange)) {
oldRebuildOnChange = JSON.parse(JSON.stringify(rebuildOnChange))
createGraph()
}
}
function buildStartNode() {
const firstNode = getFirstNode(nodes)
if (!firstNode) {
return
}
const startNodeConfig = {
id: 'start',
data: {
custom: {
component: DecisionTreeGraphHeader,
props: {
node: {
id: 'start',
label: 'Start',
next: {
id: firstNode.id,
condition: {
type: 'evalv2',
expr: 'true',
fieldType: 'boolean'
}
}
},
canDelete: false,
label: 'Start'
},
cb: (e: string, detail: any) => nodeCallbackHandler(e, detail, undefined, [], false)
}
}
}
const startNode = createNode(startNodeConfig)
displayedNodes.push(startNode)
edges.push(
createEdge({
id: `start-${firstNode?.id}`,
source: 'start',
target: firstNode?.id
})
)
}
const { app, runnableComponents, componentControl, debuggingComponents } =
getContext<AppViewerContext>('AppViewerContext')
function buildEndNode() {
const lastNodesIds = nodes
.filter((node) => {
return node.next.length == 0
})
.map((node) => node.id)
displayedNodes.push(
createNode({
id: 'end',
data: {
custom: {
component: DecisionTreeGraphHeader,
props: {
node: {
id: 'end',
label: 'End',
next: []
},
canDelete: false,
isTail: true,
label: 'End'
},
cb: (e: string, detail: any) => {
if (e == 'select') {
$selectedNodeId = detail
}
}
}
},
parentIds: lastNodesIds
})
)
lastNodesIds.forEach((lastNodeId) => {
edges.push(
createEdge({
id: `${lastNodeId}-end`,
source: lastNodeId,
target: 'end'
})
)
})
}
function addSubGrid() {
const numberOfPanes = nodes.length
if (!$app.subgrids) {
@@ -258,149 +158,231 @@
dispatch('render')
}
function buildGraphNodes() {
nodes?.forEach((graphNode, index) => {
const parentIds = getParents(nodes, graphNode.id)
const parentNext = nodes.find((node) => node.id == parentIds[0])?.next
const hasParentBranches = parentNext ? parentNext.length > 1 : false
function graphBuilder(decisionTreeNodes: DecisionTreeNode[]) {
const nodes: Node[] = []
const edges: Edge[] = []
if (hasParentBranches) {
const positionRelativeToParent = parentNext?.findIndex((next) => next.id == graphNode.id)
const branchHeaderId = `${parentIds[0]}-${graphNode.id}-branch-header`
function addNode(
node: DecisionTreeNode,
type: string = 'step',
data: {
canDelete: boolean
canAddBranch: boolean
index: number
parentIds?: string[]
},
x = nodeCallbackHandler
) {
nodes.push({
id: node.id,
type,
position: { x: -1, y: -1 },
data: {
node,
nodeCallbackHandler: x,
...data
}
})
}
// We create a header node for the branch, which will be the parent of the actual node
displayedNodes.push(
createNode({
const parents: { [key: string]: string[] } = {}
function addEdge(source: string, target: string) {
parents[target] = [...(parents[target] ?? []), source]
edges.push({
id: `${source}-${target}`,
source,
target,
type: 'edge'
})
}
function processNodes(decisionTreeNodes: DecisionTreeNode[]) {
decisionTreeNodes.forEach((graphNode, index) => {
const parentIds = getParents(decisionTreeNodes, graphNode.id)
const parentNext = decisionTreeNodes.find((node) => node.id == parentIds[0])?.next
const hasParentBranches = parentNext ? parentNext.length > 1 : false
if (hasParentBranches) {
const positionRelativeToParent = parentNext?.findIndex((next) => next.id == graphNode.id)
const branchHeaderId = `${parentIds[0]}-${graphNode.id}-branch-header`
// We create a header node for the branch, which will be the parent of the actual node
const header = {
id: branchHeaderId,
type: 'start',
position: { x: -1, y: -1 },
data: {
custom: {
component: DecisionTreeGraphHeader,
props: {
node: graphNode,
canDelete: true,
label:
positionRelativeToParent === 0
? 'Default branch'
: `Branch ${positionRelativeToParent}`
},
cb: (e: string, detail: any) => {
nodeCallbackHandler(e, detail, graphNode, parentIds, true)
}
}
node: {
label:
positionRelativeToParent === 0
? 'Default branch'
: `Branch ${positionRelativeToParent}`,
id: branchHeaderId,
allowed: undefined,
next: [],
parentIds: [parentIds[0]]
},
canDelete: true,
nodeCallbackHandler: (e, d) => {
nodeCallbackHandler(e, d, graphNode, parentIds, true)
},
branchHeader: true
}
}
nodes.push(header)
const cannotAddBranch =
graphNode.next.length === 0 ||
(graphNode.next.length === 1 &&
getParents(decisionTreeNodes, graphNode.next[0].id).length > 1)
// We create the actual node
addNode(
graphNode,
'step',
{
canDelete: !(graphNode.next.length > 1 && parentIds.length > 1),
canAddBranch: !cannotAddBranch,
index,
parentIds: [
branchHeaderId,
...parentIds.filter((pId) => {
const firstLetter = branchHeaderId.split('-')[0]
return pId !== firstLetter
})
]
},
parentIds: [parentIds[0]]
})
)
const cannotAddBranch =
graphNode.next.length === 0 ||
(graphNode.next.length === 1 && getParents(nodes, graphNode.next[0].id).length > 1)
// We create the actual node
displayedNodes.push(
createNode({
id: graphNode.id,
data: {
custom: {
component: DecisionTreeGraphNode,
props: {
node: graphNode,
canDelete: !(graphNode.next.length > 1 && parentIds.length > 1),
canAddBranch: !cannotAddBranch,
index,
darkMode
},
cb: (e: string, detail: any) =>
nodeCallbackHandler(e, detail, graphNode, parentIds, false)
}
},
parentIds: [
branchHeaderId,
...parentIds.filter((pId) => {
const firstLetter = branchHeaderId.split('-')[0]
return pId !== firstLetter
})
]
})
)
// Edge between branch header and node
edges.push(
createEdge({
id: `${graphNode.id}-${branchHeaderId}`,
source: branchHeaderId,
target: graphNode.id
})
)
if (graphNode.next.length === 1) {
edges.push(
createEdge({
id: `${branchHeaderId}-${graphNode.next[0].id}`,
source: graphNode.id,
target: graphNode.next[0].id
})
(e, d) => {
nodeCallbackHandler(e, d, graphNode, parentIds, false)
return undefined
}
)
addEdge(branchHeaderId, graphNode.id)
if (graphNode.next.length === 1) {
addEdge(graphNode.id, graphNode.next[0].id)
} else {
graphNode.next.forEach((nextNode) => {
addEdge(graphNode.id, `${graphNode.id}-${nextNode.id}-branch-header`)
})
}
} else {
const cannotAddBranch =
graphNode.next.length === 0 ||
(graphNode.next.length === 1 &&
getParents(decisionTreeNodes, graphNode.next[0].id).length > 1)
addNode(
graphNode,
'step',
{
canDelete:
!cannotAddBranch && (graphNode.next.length == 1 || !parentIds.includes('start')),
canAddBranch: !cannotAddBranch,
index,
parentIds: parentIds
},
(e, d) => {
nodeCallbackHandler(e, d, graphNode, parentIds, false)
return undefined
}
)
// if node has multiple next, it means it needs to be connected to a branch header
const hasMultipleNext = graphNode.next.length > 1
graphNode.next.forEach((nextNode) => {
edges.push(
createEdge({
id: `${graphNode.id}-${nextNode.id}-branch-header-${nextNode.id}`,
source: graphNode.id,
target: `${graphNode.id}-${nextNode.id}-branch-header`
})
)
const target = hasMultipleNext
? `${graphNode.id}-${nextNode.id}-branch-header`
: nextNode.id
addEdge(graphNode.id, target)
})
}
} else {
const cannotAddBranch =
graphNode.next.length === 0 ||
(graphNode.next.length === 1 && getParents(nodes, graphNode.next[0].id).length > 1)
})
}
displayedNodes.push(
createNode({
id: graphNode.id,
data: {
custom: {
component: DecisionTreeGraphNode,
props: {
node: graphNode,
canDelete:
!cannotAddBranch &&
(graphNode.next.length == 1 || !parentIds.includes('start')),
canAddBranch: !cannotAddBranch,
index,
darkMode
},
cb: (e: string, detail: any) =>
nodeCallbackHandler(e, detail, graphNode, parentIds, false)
const firstNode = getFirstNode(decisionTreeNodes)
if (firstNode) {
const startNode = {
id: 'start',
type: 'start',
position: { x: -1, y: -1 },
data: {
node: {
id: 'start',
label: 'Start',
allowed: undefined,
next: [
{
id: firstNode.id,
condition: {
type: 'evalv2',
expr: 'true',
fieldType: 'boolean'
}
}
},
parentIds: parentIds
})
)
]
},
canDelete: false,
nodeCallbackHandler
}
}
nodes.push(startNode)
addEdge('start', firstNode.id)
}
// if node has multiple next, it means it needs to be connected to a branch header
const hasMultipleNext = graphNode.next.length > 1
processNodes(decisionTreeNodes)
graphNode.next.forEach((nextNode) => {
const target = hasMultipleNext
? `${graphNode.id}-${nextNode.id}-branch-header`
: nextNode.id
const endNode = {
id: 'end',
type: 'end',
position: { x: -1, y: -1 },
data: {
node: {
id: 'end',
label: 'End',
allowed: undefined,
next: []
},
canDelete: false,
nodeCallbackHandler
}
}
edges.push(
createEdge({
id: `${graphNode.id}-${nextNode.id}`,
source: graphNode.id,
target
})
)
})
const lastNodesIds = decisionTreeNodes
.filter((node) => {
return node.next.length == 0
})
.map((node) => node.id)
lastNodesIds.forEach((id) => {
addEdge(id, endNode.id)
})
nodes.push(endNode)
Object.keys(parents).forEach((key) => {
const node = nodes.find((n) => n.id === key)
if (node) {
node.data.parentIds = parents[key]
}
})
$nodesStore = layoutNodes(nodes)
$edgesStore = edges
}
function layoutNodes(nodes: Node[]): { nodes: Node[]; height: number; width: number } {
$: graphBuilder(nodes)
function layoutNodes(nodes: Node[]): Node[] {
let seenId: string[] = []
for (const n of nodes) {
if (seenId.includes(n.id)) {
@@ -408,8 +390,14 @@
}
seenId.push(n.id)
}
const flattenParentIds = nodes.map((n) => ({
...n,
parentIds: n.data?.parentIds ?? []
})) as any
const stratify = dagStratify().id(({ id }: Node) => id)
const dag = stratify(nodes)
const dag = stratify(flattenParentIds)
let boxSize: any
try {
@@ -425,73 +413,56 @@
boxSize = layout(dag)
}
return {
nodes: dag.descendants().map((des) => {
return {
...des.data,
id: des.data.id,
position: {
x: des.x ? des.x + paneWidth / 2 - boxSize.width / 2 - NODE.width / 2 : 0,
y: des.y || 0
}
}
}),
height: boxSize.height + NODE.height,
width: boxSize.width + NODE.width
}
const newNodes = dag.descendants().map((des) => ({
...des.data,
id: des.data.id,
position: {
x: des.x ? des.x - boxSize.width / 2 - NODE.width / 2 : 0,
y: des.y || 0
}
}))
return newNodes
}
function resetGraphData() {
displayedNodes = []
edges = []
}
function createGraph() {
try {
resetGraphData()
buildStartNode()
buildGraphNodes()
buildEndNode()
applyLayoutToNodes()
} catch (e) {
console.error(e)
}
}
function applyLayoutToNodes() {
const layered = layoutNodes(displayedNodes)
displayedNodes = layered.nodes
}
let mounted = false
onMount(() => {
setTimeout(() => {
createGraph()
}, 10)
mounted = true
const viewport = writable<Viewport>({
x: 0,
y: 5,
zoom: 1
})
$: if (nodes.length > 0 && !$selectedNodeId) {
$selectedNodeId = getFirstNode(nodes)?.id
function centerViewport(width: number) {
viewport.update((vp) => ({
...vp,
x: width / 2,
y: vp.y
}))
}
$: paneWidth && centerViewport(paneWidth)
const nodeTypes = {
step: DecisionTreeGraphNode,
start: DecisionTreeGraphHeader,
end: DecisionTreeGraphHeader
} as any
</script>
<DarkModeObserver bind:darkMode />
<SvelteFlow
nodes={nodesStore}
edges={edgesStore}
{nodeTypes}
{viewport}
height={paneHeight}
minZoom={0.5}
connectionLineType={ConnectionLineType.SmoothStep}
defaultEdgeOptions={{ type: 'smoothstep' }}
zoomOnDoubleClick={false}
elementsSelectable={false}
proOptions={{ hideAttribution: true }}
nodesDraggable={false}
>
<div class="absolute inset-0 !bg-surface-secondary" />
{#if mounted}
<Svelvet
download={false}
highlightEdges={false}
locked
dataflow={false}
nodes={displayedNodes}
{edges}
height={paneHeight}
{scroll}
nodeSelected={false}
background={false}
width={paneWidth}
/>
{/if}
<Controls position="top-right" orientation="horizontal" showLock={false} />
</SvelteFlow>
@@ -1,4 +1,5 @@
import { NODE, type Node } from '$lib/components/graph'
import { NODE } from '$lib/components/graph'
import { type Node } from '@xyflow/svelte'
interface NodeConfig {
id: string
@@ -35,12 +36,7 @@ export function createNode(nodeConfig: NodeConfig): Node {
position: nodeConfig.position || { x: -1, y: -1 },
data: nodeConfig.data,
width: nodeConfig.width || NODE.width,
height: nodeConfig.height || NODE.height,
borderColor: nodeConfig.borderColor || '#999',
sourcePosition: 'bottom',
targetPosition: 'top',
parentIds: nodeConfig.parentIds || [],
loopDepth: nodeConfig.loopDepth || 0
height: nodeConfig.height || NODE.height
}
}
@@ -1,7 +1,8 @@
<script lang="ts">
import { Menu } from '$lib/components/common'
import Menu from '$lib/components/common/menu/MenuV2.svelte'
import { createEventDispatcher } from 'svelte'
import { ListFilter } from 'lucide-svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
const dispatch = createEventDispatcher()
@@ -26,50 +27,44 @@
}
</script>
<Menu
transitionDuration={0}
pointerDown
bind:show={open}
noMinW
placement="bottom-center"
let:close
>
<button
title="Pick an iteration"
slot="trigger"
id={`flow-editor-iteration picker-${index}`}
type="button"
class=" text-xs bg-surface border-[1px] border-gray-300 dark:border-gray-500 focus:outline-none
<Menu>
<div slot="trigger">
<button
title="Pick an iteration"
id={`flow-editor-iteration picker-${index}`}
type="button"
class=" text-xs bg-surface border-[1px] border-gray-300 dark:border-gray-500 focus:outline-none
hover:bg-surface-hover focus:ring-4 focus:ring-surface-selected font-medium rounded-sm w-[40px] gap-1 h-[20px]
flex items-center justify-center {flowJobsSuccess?.[selected] == false
? 'text-red-400'
: 'text-secondary'}"
>
#{selected == -1 ? '?' : selected + 1}
<ListFilter size={15} />
</button>
<div id="flow-editor-insert-module">
<div class="font-mono divide-y text-xs w-full text-secondary max-h-[200px] overflow-auto">
<input autofocus type="number" bind:value={filter} on:keydown={onKeydown} />
{#each flowJobs ?? [] as id, idx (id)}
{#if filter == undefined || (idx + 1).toString().includes(filter.toString())}
<button
class="w-full text-left py-1 pl-2 min-w-20 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center {flowJobsSuccess?.[
idx
] == false
? 'text-red-400'
: ''}"
on:pointerdown={() => {
close()
dispatch('selectedIteration', { index: idx, id })
}}
role="menuitem"
tabindex="-1"
>
#{idx + 1}
</button>
{/if}
{/each}
</div>
? 'text-red-400'
: 'text-secondary'}"
>
#{selected == -1 ? '?' : selected + 1}
<ListFilter size={15} />
</button>
</div>
<MenuItem disabled>
<input type="number" bind:value={filter} on:keydown={onKeydown} />
</MenuItem>
{#each flowJobs ?? [] as id, idx (id)}
{#if filter == undefined || (idx + 1).toString().includes(filter.toString())}
<MenuItem>
<button
class="text-primary text-xs w-full text-left py-1 pl-2 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center {flowJobsSuccess?.[
idx
] == false
? 'text-red-400'
: ''}"
on:pointerdown={() => {
//close()
dispatch('selectedIteration', { index: idx, id })
}}
role="menuitem"
tabindex="-1"
>
#{idx + 1}
</button>
</MenuItem>
{/if}
{/each}
</Menu>
@@ -147,9 +147,8 @@
>
{#if $$slots.icon}
<slot name="icon" />
<span class="mr-2" />
{/if}
<div class="flex-1 truncate" class:font-bold={bold}>{label}</div>
<div class="truncate" class:font-bold={bold}>{label}</div>
<div class="flex items-center space-x-2">
{#if id}
<Badge color="indigo">{id}</Badge>
@@ -16,7 +16,6 @@
import FlowConstantsItem from './FlowConstantsItem.svelte'
import { dfs } from '../dfs'
import { FlowGraph } from '$lib/components/graph'
import FlowErrorHandlerItem from './FlowErrorHandlerItem.svelte'
import { push } from '$lib/history'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
@@ -30,6 +29,7 @@
import FlowTutorials from '$lib/components/FlowTutorials.svelte'
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
import { tutorialInProgress } from '$lib/tutorialUtils'
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
export let modules: FlowModule[] | undefined
export let sidebarSize: number | undefined = undefined
@@ -252,13 +252,12 @@
</div>
<div class="z-10 flex-auto grow" bind:clientHeight={minHeight}>
<FlowGraph
<FlowGraphV2
{disableAi}
insertable
scroll
{minHeight}
moving={$moving?.module.id}
rebuildOnChange={$flowStore}
maxHeight={minHeight}
modules={$flowStore.value?.modules}
{selectedId}
@@ -268,7 +267,9 @@
dependents = getDependentComponents(e.id, $flowStore)
const cb = () => {
push(history, $flowStore)
selectNextId(e.id)
removeAtId($flowStore.value.modules, e.id)
if ($flowInputsStore) {
@@ -6,6 +6,7 @@
import type { FlowModule } from '$lib/gen'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import type { WhitelabelCustomUi } from '$lib/components/custom_ui'
import { twMerge } from 'tailwind-merge'
const dispatch = createEventDispatcher()
export let trigger = false
@@ -28,15 +29,21 @@
placement="bottom-center"
let:close
>
<button
title="Add step"
slot="trigger"
id={`flow-editor-add-step-${index}`}
type="button"
class="text-primary bg-surface border-[1px] mx-[1px] border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-surface-selected font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<Cross class="mx-[5px]" size={15} />
</button>
<svelte:fragment slot="trigger">
<button
title="Add step"
id={`flow-editor-add-step-${index}`}
type="button"
class={twMerge(
'w-6 h-6 flex items-center justify-center',
'border border-gray-300 dark:border-gray-500',
'text-primary text-sm',
'bg-surface focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-surface-selected rounded-full '
)}
>
<Cross size={14} />
</button>
</svelte:fragment>
<div id="flow-editor-insert-module">
<StepGen on:insert {index} bind:funcDesc bind:open {close} {modules} {disableAi} />
@@ -3,36 +3,23 @@
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
import type { FlowModule } from '$lib/gen'
import {
Building,
ClipboardCopy,
GitBranchPlus,
Repeat,
Square,
ArrowDown,
GitBranch
} from 'lucide-svelte'
import { Building, Repeat, Square, ArrowDown, GitBranch } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import type { Writable } from 'svelte/store'
import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte'
import InsertModuleButton from './InsertModuleButton.svelte'
import { prettyLanguage } from '$lib/common'
import { msToSec } from '$lib/utils'
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
import FlowJobsMenu from './FlowJobsMenu.svelte'
export let mod: FlowModule
export let trigger: boolean
export let insertable: boolean
export let insertableEnd = false
export let annotation: string | undefined = undefined
export let branchable: boolean = false
export let bgColor: string = ''
export let modules: FlowModule[]
export let moving: string | undefined = undefined
export let duration_ms: number | undefined = undefined
export let disableAi: boolean = false
export let wrapperId: string | undefined = undefined
export let retries: number | undefined = undefined
export let flowJobs:
| { flowJobs: string[]; selected: number; flowJobsSuccess: (boolean | undefined)[] }
@@ -69,46 +56,9 @@
function onDelete(event: CustomEvent<MouseEvent>) {
dispatch('delete', event)
}
let openMenu: boolean | undefined = undefined
let openMenu2: boolean | undefined = undefined
</script>
{#if mod}
{#if insertable}
<div
class="{openMenu
? 'z-20'
: ''} w-[27px] absolute -top-[35px] left-[50%] right-[50%] -translate-x-1/2"
>
{#if moving}
<button
title="Add branch"
on:click={() => {
dispatch('insert', { modules, index: idx, detail: 'move' })
}}
type="button"
disabled={wrapperId === moving}
class=" text-primary bg-surface border mx-[1px] border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<ClipboardCopy class="m-[5px]" size={15} />
</button>
{:else}
<InsertModuleButton
{disableAi}
bind:open={openMenu}
{trigger}
on:insert={(e) => {
dispatch('insert', { modules, index: idx + 1, detail: 'script', script: e.detail })
}}
on:new={(e) => {
dispatch('insert', { modules, index: idx, detail: e.detail })
}}
index={idx}
{modules}
/>
{/if}
</div>
{/if}
<div class="relative">
{#if moving == mod.id}
<div class="absolute z-10 right-20 top-0.5 center-center">
@@ -128,8 +78,8 @@
{annotation}
</div>
{/if}
{#if flowJobs && !insertable}
<div class="absolute z-10 right-8 -top-5">
{#if flowJobs && !insertable && (mod.value.type === 'forloopflow' || mod.value.type === 'whileloopflow')}
<div class="absolute right-8 z-50 -top-5">
<FlowJobsMenu
on:selectedIteration={(e) => {
dispatch('selectedIteration', e.detail)
@@ -242,55 +192,4 @@
{/if}
</div>
</div>
{#if insertable && insertableEnd}
<div
class="{openMenu2
? 'z-20'
: ''} w-[27px] absolute top-[49px] left-[50%] right-[50%] -translate-x-1/2"
>
{#if moving}
<button
title="Add branch"
on:click={() => {
dispatch('insert', { modules, index: idx + 1, detail: 'move' })
}}
type="button"
disabled={wrapperId === moving}
class=" text-primary bg-surface border mx-[1px] border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<ClipboardCopy class="m-[5px]" size={15} />
</button>
{:else}
<InsertModuleButton
{disableAi}
bind:open={openMenu2}
{trigger}
on:insert={(e) => {
dispatch('insert', { modules, index: idx + 1, detail: 'script', script: e.detail })
}}
on:new={(e) => {
dispatch('insert', { modules, index: idx + 1, detail: e.detail })
}}
index={idx + 1}
{modules}
/>
{/if}
</div>
{/if}
{#if insertable && branchable}
<div class="w-[27px] absolute top-[45px] left-[60%] right-[40%] -translate-x-1/2">
<button
title="Add branch"
on:click={() => {
dispatch('newBranch', { module: mod })
}}
type="button"
id="add-branch-button"
class=" text-primary bg-surface border mx-[1px] rotate-180 dark:border-gray-500 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<GitBranchPlus class="m-[5px]" size={15} />
</button>
</div>
{/if}
{/if}
@@ -1,29 +1,16 @@
<script lang="ts">
import { base } from '$lib/base'
import { Badge } from '$lib/components/common'
import type { FlowModule } from '$lib/gen'
import { classNames } from '$lib/utils'
import { ClipboardCopy, ExternalLink, Wand2, X } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import InsertModuleButton from './InsertModuleButton.svelte'
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import { copilotInfo } from '$lib/stores'
import Menu from '$lib/components/common/menu/Menu.svelte'
import InsertTriggerButton from './InsertTriggerButton.svelte'
export let label: string
export let modules: FlowModule[] | undefined
export let index: number
export let insertable: boolean
export let bgColor: string = ''
export let selected: boolean
export let selectable: boolean
export let deleteBranch: { module: FlowModule; index: number } | undefined = undefined
export let id: string | undefined = undefined
export let moving: string | undefined = undefined
export let center = true
export let disableAi: boolean = false
export let wrapperNode: FlowModule | undefined = undefined
export let borderColor: string | undefined = undefined
export let hideId: boolean = false
@@ -35,32 +22,12 @@
index: number
}
select: string
deleteBranch: { module: FlowModule; index: number }
}>()
let openMenu = false
let triggerOpenMenu = false
let openNoCopilot = false
const { drawerStore: copilotDrawerStore, currentStepStore: copilotCurrentStepStore } =
const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
</script>
{#if insertable && deleteBranch}
<div class="w-[27px] absolute -top-[40px] left-[50%] right-[50%] -translate-x-1/2">
<button
title="Delete branch"
on:click|stopPropagation={() => {
if (deleteBranch) {
dispatch('deleteBranch', deleteBranch)
}
}}
type="button"
class="text-primary bg-surface border mx-[1px] 'border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<X class="m-[5px]" size={15} />
</button>
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
@@ -68,9 +35,10 @@
'w-full flex relative overflow-hidden rounded-sm',
selectable ? 'cursor-pointer' : '',
selected ? 'outline outline-offset-1 outline-2 outline-gray-600' : '',
label === 'Input' && $copilotCurrentStepStore === 'Input' ? 'z-[901]' : ''
label === 'Input' && $copilotCurrentStepStore === 'Input' ? 'z-[901]' : '',
'bg-surface'
)}
style="min-width: 275px; max-height: 80px; background-color: {bgColor};"
style="width: 275px; max-height: 34px; background-color: {bgColor} !important;"
on:click={() => {
if (selectable) {
if (id) {
@@ -93,7 +61,7 @@
<span class="mr-2" />
{/if}
<div />
<div class="flex-1 truncate"><pre>{label}</pre></div>
<div class="truncate"><pre>{label}</pre></div>
<div class="flex items-center space-x-2">
{#if id && !hideId}
<Badge color="indigo">{id}</Badge>
@@ -101,125 +69,3 @@
</div>
</div>
</div>
{#if insertable && modules && (label != 'Input' || modules.length == 0)}
<div
class="{openMenu
? 'z-20'
: ''} w-[27px] absolute top-[49px] left-[50%] right-[50%] -translate-x-1/2"
>
{#if moving}
<button
title="Add branch"
on:click={() => {
if (modules) {
dispatch('insert', {
modules,
index,
detail: 'move'
})
}
}}
type="button"
disabled={wrapperNode?.id === moving}
class="text-primary bg-surface border mx-[1px] border-gray-300 dark:border-gray-500 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-[25px] h-[25px] flex items-center justify-center"
>
<ClipboardCopy size={12} />
</button>
{:else}
<InsertModuleButton
{disableAi}
bind:open={openMenu}
trigger={label == 'Input'}
on:insert={(e) => {
if (modules) {
dispatch('insert', {
modules,
index,
detail: 'script',
script: e.detail
})
}
}}
on:new={(e) => {
if (modules) {
dispatch('insert', {
modules,
index: index,
detail: e.detail
})
}
}}
{index}
modules={modules ?? []}
/>
{/if}
</div>
{/if}
{#if insertable && modules && label == 'Input'}
{#if !disableAi}
<div
class="{openNoCopilot
? 'z-10'
: ''} w-9 absolute -top-10 left-[50%] right-[50%] -translate-x-1/2"
>
<Menu pointerDown noMinW placement="bottom-center" let:close bind:show={openNoCopilot}>
<button
title="AI Flow Builder"
on:pointerdown={$copilotInfo.exists_openai_resource_path
? (ev) => {
ev.preventDefault()
ev.stopPropagation()
$copilotDrawerStore?.openDrawer()
}
: undefined}
slot="trigger"
type="button"
class=" bg-surface text-violet-800 dark:text-violet-400 border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-8 h-8 flex items-center justify-center"
>
<Wand2 size={16} />
</button>
{#if !$copilotInfo.exists_openai_resource_path}
<div class="text-primary p-4">
<p class="text-sm w-80">
Enable Windmill AI in the
<a
href="{base}/workspace_settings?tab=openai"
target="_blank"
class="inline-flex flex-row items-center gap-1"
on:click={() => {
close()
}}
>
workspace settings
<ExternalLink size={16} />
</a>
</p>
</div>
{/if}
</Menu>
</div>
{/if}
<div
class="{triggerOpenMenu
? 'z-10'
: ''} w-[27px] absolute top-[50px] left-[65%] right-[35%] -translate-x-1/2"
>
<InsertTriggerButton
{disableAi}
bind:open={triggerOpenMenu}
on:new={(e) => {
if (modules) {
dispatch('insert', {
modules,
index: 0,
detail: e.detail
})
}
}}
index={0}
modules={modules ?? []}
/>
</div>
{/if}
@@ -1,892 +0,0 @@
<script lang="ts">
import { sugiyama, dagStratify, decrossOpt, coordCenter } from 'd3-dag'
import { type FlowModule, type FlowStatusModule } from '../../gen'
import {
NODE,
createIdGenerator,
isNode,
isLoop,
isBranch,
type GraphItem,
type Node,
type Loop,
type Branch,
type NestedNodes,
type GraphModuleState,
getStateColor
} from '.'
import { defaultIfEmptyString, encodeState } from '$lib/utils'
import { createEventDispatcher, onMount, setContext } from 'svelte'
import Svelvet from './svelvet/container/views/Svelvet.svelte'
import type { UserEdgeType } from './svelvet/types'
import MapItem from '../flows/map/MapItem.svelte'
import VirtualItem from '../flows/map/VirtualItem.svelte'
import { writable, type Writable } from 'svelte/store'
import { getDependeeAndDependentComponents } from '../flows/flowExplorer'
import { deepEqual } from 'fast-equals'
import DarkModeObserver from '../DarkModeObserver.svelte'
import type { FlowInput } from '../flows/types'
import { dfsByModule } from '../flows/previousResults'
export let success: boolean | undefined = undefined
export let modules: FlowModule[] | undefined = []
export let failureModule: FlowModule | undefined = undefined
export let minHeight: number = 0
export let maxHeight: number | undefined = undefined
export let notSelectable = false
export let flowModuleStates: Record<string, GraphModuleState> | undefined = undefined
export let rebuildOnChange: any = undefined
export let selectedId: Writable<string | undefined> = writable<string | undefined>(undefined)
export let insertable = false
export let moving: string | undefined = undefined
export let scroll = false
export let download = false
export let fullSize = false
export let disableAi = false
export let flowInputsStore: Writable<FlowInput | undefined> = writable<FlowInput | undefined>(
undefined
)
setContext<{
selectedId: Writable<string | undefined>
flowInputsStore: Writable<FlowInput | undefined>
}>('FlowGraphContext', { selectedId, flowInputsStore })
let idGenerator: Generator
let nestedNodes: NestedNodes
let nodes: Node[] = []
let edges: UserEdgeType[] = []
let width: number, height: number
let fullWidth: number
let errorHandlers: Record<string, string> = {}
let containerHeight = window.innerHeight
let error: string | undefined = undefined
$: showDataflow =
$selectedId != undefined &&
!$selectedId.startsWith('constants') &&
!$selectedId.startsWith('settings') &&
$selectedId !== 'failure' &&
$selectedId !== 'Result'
let dataflow = false
let dispatch = createEventDispatcher()
let renderCount = 1
$: {
dataflow
moving
success
width && height && minHeight && $selectedId && flowModuleStates && renderCount
createGraph()
}
$: rebuildOnChange && triggerRebuild()
let oldRebuildOnChange = rebuildOnChange ? JSON.parse(JSON.stringify(rebuildOnChange)) : undefined
let darkMode = false
function triggerRebuild() {
if (!deepEqual(oldRebuildOnChange, rebuildOnChange)) {
oldRebuildOnChange = JSON.parse(JSON.stringify(rebuildOnChange))
createGraph()
}
}
async function createGraph() {
try {
// console.log(JSON.stringify(modules))
// return
nodes = []
edges = []
errorHandlers = {}
if (modules) {
idGenerator = createIdGenerator()
} else {
nodes = edges = []
return
}
nestedNodes = nodes = []
nestedNodes.push(
createVirtualNode(
getParentIds(),
'Input',
modules,
undefined,
undefined,
0,
0,
true,
undefined,
undefined,
'Input',
undefined,
undefined
)
)
modules.forEach((m, i) => {
const item = getConvertedFlowModule(
m,
undefined,
undefined,
0,
i + 1 == modules?.length,
modules!
)
item && nestedNodes.push(item)
})
nestedNodes.push(
createVirtualNode(
getParentIds(),
'Result',
undefined,
undefined,
undefined,
0,
modules.length,
true,
undefined,
undefined,
undefined,
success == undefined ? undefined : success ? 'Success' : 'Failure',
undefined
)
)
if (!flowModuleStates) {
if (failureModule) nestedNodes.push(createErrorHandler(failureModule))
} else {
Object.entries(flowModuleStates ?? [])
.filter(([k, v]) => k.startsWith('failure'))
.forEach(([k, v]) => {
nestedNodes.push(createErrorHandler({ id: k } as FlowModule, v.parent_module, k))
})
}
const flatNodes = flattenNestedNodes(nestedNodes)
const layered = layoutNodes(flatNodes)
nodes = layered.nodes
let hfull = Math.max(layered.height, minHeight)
fullWidth = layered.width
height = fullSize ? hfull : Math.max(hfull, maxHeight ?? containerHeight)
let useDataflow = dataflow && showDataflow
edges = useDataflow ? [] : createEdges(nodes)
if (useDataflow && $selectedId) {
let deps = getDependeeAndDependentComponents($selectedId, modules ?? [], failureModule)
if (deps) {
Object.entries(deps.dependees).forEach((x, i) => {
const inputs = x[1]
inputs?.forEach((input, index) => {
let pid = x[0]
if (input?.startsWith('flow_input.iter')) {
const parent = dfsByModule($selectedId!, modules ?? [])?.pop()
if (parent?.id) {
pid = parent.id
}
}
edges.push({
id: `dep-${pid}-${$selectedId}-${input}`,
source: pid,
target: $selectedId!,
labelBgColor: darkMode ? '#999' : 'white',
edgeColor: darkMode ? 'white' : 'black',
arrow: false,
animate: true,
noHandle: true,
label: pid,
type: 'bezier',
offset: index * 20
})
})
})
Object.entries(deps.dependents).forEach((x, i) => {
let pid = x[0]
edges.push({
id: `dep-${pid}-${$selectedId}`,
source: $selectedId!,
target: pid,
labelBgColor: darkMode ? '#999' : 'white',
edgeColor: darkMode ? 'white' : 'black',
arrow: false,
animate: true,
noHandle: true,
label: pid,
type: 'bezier',
offset: i * 10
})
})
}
}
if (error) error = undefined
} catch (e) {
error = e.message
}
}
function getConvertedFlowModule(
module: FlowModule,
parent: NestedNodes | string | undefined,
edgeLabel: string | undefined,
loopDepth: number,
insertableEnd: boolean,
modules: FlowModule[],
wrapper: FlowModule | undefined = undefined
): GraphItem | undefined {
const type = module.value.type
const parentIds = getParentIds(parent)
if (type === 'forloopflow' || type == 'whileloopflow') {
//@ts-ignore
return flowModuleToLoop(modules, module, parent, loopDepth)
} else if (type === 'branchone') {
const branches = [
{ summary: 'Default Branch', modules: module.value.default, removable: false },
...module.value.branches.map((b, i) => ({
summary: defaultIfEmptyString(b.summary, 'Branch ' + (i + 1)) + '\n`' + b.expr + '`',
modules: b.modules,
removable: true
}))
]
return flowModuleToBranch(
module,
modules,
branches,
[], //['', ...module.value.branches.map((x) => `${truncateRev(x.expr, 20)}`)],
parent,
loopDepth,
false
)
} else if (type === 'branchall') {
const branches = module.value.branches.map((b, i) => ({
summary: defaultIfEmptyString(b.summary, `Branch ${i + 1}`),
modules: b.modules,
removable: true
}))
return flowModuleToBranch(module, modules, branches, [], parent, loopDepth, true)
}
return flowModuleToNode(
parentIds,
module,
edgeLabel,
undefined,
loopDepth,
insertableEnd,
false,
modules,
wrapper,
undefined
)
}
function getParentIds(items: string | NestedNodes | undefined = undefined): string[] {
if (typeof items == 'string') {
return [items]
}
const item = items?.at(-1) || nestedNodes.at(-1)
if (!item) return []
if (isNode(item)) {
const id = item.id
return [id]
} else if (isLoop(item)) {
return getParentIds(item.items)
} else if (isBranch(item)) {
return [item.nodeEnd.id]
}
return []
}
function flowModuleToNode(
parentIds: string[],
mod: FlowModule,
edgeLabel: string | undefined,
annotation: string | undefined,
loopDepth: number,
insertableEnd: boolean,
branchable: boolean,
modules: FlowModule[],
wrapper: FlowModule | undefined,
flowJobs:
| { flowJobs: string[]; selected: number; flowJobsSuccess?: (boolean | undefined)[] }
| undefined
): Node {
let type = flowModuleStates?.[mod.id]?.type
if (!type && flowJobs) {
type = 'InProgress'
}
return {
type: 'node',
id: mod.id,
position: { x: -1, y: -1 },
data: {
custom: {
component: MapItem,
props: {
trigger: parentIds.length == 0,
mod,
insertable,
insertableEnd,
branchable,
retries: flowModuleStates?.[mod.id]?.retries,
duration_ms: flowModuleStates?.[mod.id]?.duration_ms,
bgColor: getStateColor(type, darkMode),
annotation: annotation,
modules,
moving,
disableAi,
wrapperId: wrapper?.id,
flowJobs
},
cb: (e: string, detail: any) => {
if (e == 'delete') {
dispatch('delete', detail)
} else if (e == 'select') {
if (!notSelectable) {
if ($selectedId != mod.id) {
$selectedId = mod.id
}
dispatch('select', mod)
}
} else if (e == 'insert') {
dispatch('insert', detail)
} else if (e == 'newBranch') {
dispatch('newBranch', detail)
} else if (e == 'move') {
dispatch('move', { module: mod, modules })
} else if (e == 'selectedIteration') {
dispatch('selectedIteration', { ...detail, moduleId: mod.id })
}
}
}
},
width: NODE.width,
height: NODE.height,
parentIds,
sourcePosition: 'bottom',
targetPosition: 'top',
edgeLabel,
loopDepth
}
}
function flowModuleToLoop(
modules: FlowModule[],
module: FlowModule & { value: { type: 'forloopflow' | 'whileloopflow' } },
parent: NestedNodes | string | undefined,
loopDepth: number
): Loop {
let state = flowModuleStates?.[module.id]
const loop: Loop = {
type: 'loop',
items: [
flowModuleToNode(
getParentIds(parent),
module,
undefined,
state?.flow_jobs
? 'iterations ' + state?.flow_jobs?.length + '/' + (state?.iteration_total ?? '?')
: '',
loopDepth,
false,
false,
modules,
undefined,
state?.flow_jobs
? {
flowJobs: state?.flow_jobs,
selected: state?.selectedForloopIndex ?? -1,
flowJobsSuccess: state?.flow_jobs_success
}
: undefined
)
]
}
const innerModules = module.value.modules
let borderStatus: FlowStatusModule['type'] | undefined = undefined
let success = state?.flow_jobs_success?.[state?.selectedForloopIndex ?? 0]
if (success != undefined) {
borderStatus = success ? 'Success' : 'Failure'
}
loop.items.push(
createVirtualNode(
getParentIds(loop.items),
`Do one iteration`,
innerModules,
undefined,
1000,
loopDepth + 1,
0,
false,
undefined,
undefined,
undefined,
undefined,
borderStatus,
true,
module
)
)
innerModules.forEach((innerModule, i) => {
const item = getConvertedFlowModule(
innerModule,
loop.items,
undefined,
loopDepth + 1,
i + 1 == innerModules?.length,
innerModules,
module
)
item && loop.items.push(item)
})
loop.items.push(
createVirtualNode(
getParentIds(loop.items),
`Collect result of each iteration`,
modules,
undefined,
1000,
loopDepth,
modules.findIndex((m) => m.id == module.id) + 1,
true,
undefined,
module.id,
undefined,
undefined,
flowModuleStates?.[module.id]?.type
)
)
return loop
}
function flowModuleToBranch(
module: FlowModule,
modules: FlowModule[],
branches: { summary: string; modules: FlowModule[]; removable: boolean }[],
edgesLabel: string[],
parent: string | NestedNodes | undefined = undefined,
loopDepth: number,
branchall: boolean
): Branch | Node {
const wrapper = JSON.parse(JSON.stringify(module))
const node = flowModuleToNode(
getParentIds(parent),
module,
undefined,
undefined,
loopDepth,
false,
true,
modules,
undefined,
undefined
)
const bitems: NestedNodes[] = []
const branchParent = [node.id]
if (branches.length == 0) {
bitems.push([
createVirtualNode(
branchParent,
'No branches',
undefined,
undefined,
0,
loopDepth,
0,
false,
undefined,
undefined,
undefined,
undefined,
undefined
)
])
}
branches.forEach(({ summary, modules, removable }, i) => {
const items: NestedNodes = []
let borderStatus: FlowStatusModule['type'] | undefined = undefined
if (module.value.type == 'branchall' || module.value.type == 'forloopflow') {
let flow_jobs_success = flowModuleStates?.[module.id]?.flow_jobs_success
if (!flow_jobs_success) {
borderStatus = 'WaitingForPriorSteps'
} else {
let status = flow_jobs_success?.[i]
if (status == undefined) {
borderStatus = 'WaitingForExecutor'
} else {
borderStatus = status ? 'Success' : 'Failure'
}
}
} else if (module.value.type == 'branchone') {
if (flowModuleStates?.[module.id]?.branchChosen == i) {
borderStatus = flowModuleStates?.[module.id]?.type
}
}
items.push(
createVirtualNode(
branchParent,
summary,
modules,
edgesLabel[i],
undefined,
loopDepth,
0,
true,
removable ? { module, index: i } : undefined,
module.id,
undefined,
undefined,
borderStatus,
false,
wrapper,
true
)
)
if (modules.length) {
modules.forEach((innerModule, j) => {
const item = getConvertedFlowModule(
innerModule,
items,
undefined,
loopDepth,
j + 1 == modules?.length,
modules,
module
)
item && items.push(item)
})
}
items.length && bitems.push(items)
})
return {
type: 'branch',
node,
nodeEnd: createVirtualNode(
bitems.map((i) => getParentIds(i)).flat(),
branchall ? 'Collect result of each branch' : 'Result of the chosen branch',
modules,
undefined,
0,
loopDepth,
modules.findIndex((m) => m.id == module.id) + 1,
true,
undefined,
module.id,
undefined,
undefined,
flowModuleStates?.[module.id]?.type
),
items: bitems
}
}
function flattenNestedNodes(nestedNodes: NestedNodes, nodes: Node[] = []): Node[] {
const array = nodes
nestedNodes.forEach((node) => {
if (isNode(node)) {
array.push(node)
} else if (isLoop(node)) {
flattenNestedNodes(node.items, array)
} else if (isBranch(node)) {
array.push(node.node)
node.items.forEach((item) => {
flattenNestedNodes(item, array)
})
array.push(node.nodeEnd)
}
})
return array
}
function layoutNodes(nodes: Node[]): { nodes: Node[]; height: number; width: number } {
let seenId: string[] = []
for (const n of nodes) {
if (seenId.includes(n.id)) {
n.id = n.id + '_dup'
}
seenId.push(n.id)
}
const stratify = dagStratify().id(({ id }: Node) => id)
const dag = stratify(nodes)
let boxSize: any
try {
const layout = sugiyama()
.decross(decrossOpt())
.coord(coordCenter())
.nodeSize(() => [NODE.width + NODE.gap.horizontal, NODE.height + NODE.gap.vertical])
boxSize = layout(dag)
} catch {
const layout = sugiyama()
.coord(coordCenter())
.nodeSize(() => [NODE.width + NODE.gap.horizontal, NODE.height + NODE.gap.vertical])
boxSize = layout(dag)
}
return {
nodes: dag.descendants().map((des) => ({
...des.data,
id: des.data.id,
position: {
x: des.x
? des.data.loopDepth * 50 +
des.x +
(fullSize ? fullWidth : width) / 2 -
boxSize.width / 2 -
NODE.width / 2
: 0,
y: des.y || 0
}
})),
height: boxSize.height + NODE.height,
width: boxSize.width + NODE.width
}
}
function createEdges(nodes: Node[]): UserEdgeType[] {
const edges: UserEdgeType[] = []
nodes.forEach((node) => {
node.parentIds.forEach((pid, i) => {
// skip virtual nodes such as collect result
if (
false &&
errorHandlers[pid] &&
parseInt(node.id) < 900 &&
nodes.find((x) => x.id == errorHandlers[pid])
) {
edges.push({
id: `e-${pid}-${node.id}`,
source: pid,
target: errorHandlers[pid],
labelBgColor: 'white',
arrow: false,
animate: false,
noHandle: true,
label: node.edgeLabel,
type: 'bezier'
})
edges.push({
id: `e-${pid}-${node.id}`,
source: errorHandlers[pid],
target: node.id,
labelBgColor: 'white',
arrow: false,
animate: false,
noHandle: true,
label: node.edgeLabel,
type: 'bezier'
})
} else {
edges.push({
id: `e-${pid}-${node.id}`,
source: pid,
target: node.id,
labelBgColor: 'white',
arrow: false,
animate: false,
noHandle: true,
label: node.edgeLabel,
edgeColor: '#999',
type: 'bezier'
})
}
})
})
return edges
}
function createVirtualNode(
parentIds: string[],
label: string,
modules: FlowModule[] | undefined,
edgeLabel: string | undefined,
offset: number | undefined,
loopDepth: number,
index: number,
selectable: boolean,
deleteBranch: { module: FlowModule; index: number } | undefined,
mid: string | undefined,
fixed_id: string | undefined,
module_status: FlowStatusModule['type'] | undefined,
borderStatus: FlowStatusModule['type'] | undefined,
center: boolean = true,
wrapperNode: FlowModule | undefined = undefined,
hideId: boolean = false
): Node {
const id = fixed_id ?? -idGenerator.next().value - 2 + (offset ?? 0)
let bgColor
if (module_status) {
bgColor = getStateColor(module_status, darkMode)
} else {
bgColor = darkMode ? '#2e3440' : '#dfe6ee'
}
return {
type: 'node',
id: id.toString(),
position: { x: -1, y: -1 },
data: {
// html: `
// <div class="w-full max-h-full text-center ellipsize-multi-line text-2xs [-webkit-line-clamp:2] px-1">
// ${label}
// </div>
// `
custom: {
component: VirtualItem,
props: {
label,
insertable,
modules,
bgColor,
borderColor: borderStatus
? getStateColor(borderStatus, darkMode) + (!darkMode ? '; border-width: 3px' : '')
: undefined,
selected: $selectedId == label,
index,
selectable,
deleteBranch,
id: mid,
moving,
center,
disableAi,
wrapperNode,
hideId
},
cb: (e: string, detail: any) => {
if (e == 'insert') {
dispatch('insert', detail)
} else if (e == 'select') {
$selectedId = detail
dispatch('select', detail)
} else if (e == 'deleteBranch') {
$selectedId = label
dispatch('deleteBranch', detail)
}
}
}
},
width: NODE.width,
height: NODE.height,
borderColor: $selectedId == label ? 'black' : '#999',
sourcePosition: 'bottom',
targetPosition: 'top',
parentIds,
edgeLabel,
loopDepth
}
}
function createErrorHandler(
mod: FlowModule,
parent_module?: string,
customNodeId?: string | undefined
): Node {
// When needed, we can add a custom node id to the error handler
// used for nested error handlers in for loop for example
const nId = customNodeId ?? 'failure'
parent_module && (errorHandlers[parent_module] = nId)
let label = 'Error handler'
return {
type: 'node',
id: nId,
position: { x: -1, y: -1 },
data: {
custom: {
component: VirtualItem,
props: {
label,
insertable: false,
modules: undefined,
bgColor: getStateColor(flowModuleStates?.[mod.id]?.type, darkMode),
selected: $selectedId == mod.id,
index: 0,
selectable: true,
id: mod.id,
disableAi
},
cb: (e: string, detail: any) => {
if (e == 'select') {
$selectedId = detail
dispatch('select', detail)
}
}
}
},
width: NODE.width,
height: NODE.height,
sourcePosition: 'bottom',
targetPosition: 'top',
parentIds: parent_module ? [parent_module] : [],
loopDepth: 0
}
}
function onThemeChange() {
renderCount++
}
onMount(() => {
onThemeChange()
})
</script>
<DarkModeObserver bind:darkMode on:change={onThemeChange} />
<!-- {JSON.stringify(flowModuleStates)} -->
<div
bind:clientWidth={width}
bind:clientHeight={containerHeight}
class={fullSize ? '' : 'w-full h-full overflow-hidden relative'}
id="flow-graph"
>
{#if !error}
{#if width && height}
{#key renderCount}
<Svelvet
on:expand={() => {
try {
localStorage.setItem('svelvet', encodeState({ modules, failureModule }))
} catch (e) {
console.error('error interacting with local storage', e)
}
window.open('/view_graph', '_blank')
}}
{download}
highlightEdges={false}
locked
bind:dataflow
{nodes}
width={fullSize ? fullWidth : width}
{edges}
{height}
{scroll}
nodeSelected={showDataflow}
background={false}
/>
{/key}
{/if}
{:else}
<div class="p-4 text-center text-xl text-red-600">
Error computing the graph: {error}
</div>
{/if}
</div>
@@ -0,0 +1,305 @@
<script lang="ts">
import { type FlowModule } from '../../gen'
import { NODE, type GraphModuleState } from '.'
import { createEventDispatcher, setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import '@xyflow/svelte/dist/style.css'
import type { FlowInput } from '../flows/types'
import {
SvelteFlow,
type Node,
type Edge,
ConnectionLineType,
Controls,
ControlButton,
type Viewport
} from '@xyflow/svelte'
import graphBuilder from './graphBuilder'
import ModuleNode from './renderers/nodes/ModuleNode.svelte'
import InputNode from './renderers/nodes/InputNode.svelte'
import BranchAllStart from './renderers/nodes/BranchAllStart.svelte'
import BranchAllEndNode from './renderers/nodes/branchAllEndNode.svelte'
import ForLoopEndNode from './renderers/nodes/ForLoopEndNode.svelte'
import ForLoopStartNode from './renderers/nodes/ForLoopStartNode.svelte'
import ResultNode from './renderers/nodes/ResultNode.svelte'
import BaseEdge from './renderers/edges/BaseEdge.svelte'
import EmptyEdge from './renderers/edges/EmptyEdge.svelte'
import { sugiyama, dagStratify, decrossOpt, coordCenter } from 'd3-dag'
import { Expand } from 'lucide-svelte'
import Toggle from '../Toggle.svelte'
import DataflowEdge from './renderers/edges/DataflowEdge.svelte'
import { encodeState } from '$lib/utils'
import BranchOneStart from './renderers/nodes/BranchOneStart.svelte'
import NoBranchNode from './renderers/nodes/NoBranchNode.svelte'
export let success: boolean | undefined = undefined
export let modules: FlowModule[] | undefined = []
export let failureModule: FlowModule | undefined = undefined
export let minHeight: number = 0
export let maxHeight: number | undefined = undefined
export let notSelectable = false
export let flowModuleStates: Record<string, GraphModuleState> | undefined = undefined
export let selectedId: Writable<string | undefined> = writable<string | undefined>(undefined)
export let insertable = false
export let moving: string | undefined = undefined
export let scroll = false
// Download: display a top level button to open the graph in a new tab
export let download = false
export let fullSize = false
export let disableAi = false
export let flowInputsStore: Writable<FlowInput | undefined> = writable<FlowInput | undefined>(
undefined
)
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
setContext<{
selectedId: Writable<string | undefined>
flowInputsStore: Writable<FlowInput | undefined>
useDataflow: Writable<boolean | undefined>
}>('FlowGraphContext', { selectedId, flowInputsStore, useDataflow })
const dispatch = createEventDispatcher()
let fullWidth = 0
let width = 0
function layoutNodes(nodes: Node[]): Node[] {
let seenId: string[] = []
for (const n of nodes) {
if (seenId.includes(n.id)) {
n.id = n.id + '_dup'
}
seenId.push(n.id)
}
const flattenParentIds = nodes.map((n) => ({
...n,
parentIds: n.data?.parentIds ?? []
})) as any
const stratify = dagStratify().id(({ id }: Node) => id)
const dag = stratify(flattenParentIds)
let boxSize: any
try {
const layout = sugiyama()
.decross(decrossOpt())
.coord(coordCenter())
.nodeSize(() => [NODE.width + NODE.gap.horizontal, NODE.height + NODE.gap.vertical])
boxSize = layout(dag)
} catch {
const layout = sugiyama()
.coord(coordCenter())
.nodeSize(() => [NODE.width + NODE.gap.horizontal, NODE.height + NODE.gap.vertical])
boxSize = layout(dag)
}
const newNodes = dag.descendants().map((des) => ({
...des.data,
id: des.data.id,
position: {
x: des.x
? // @ts-ignore
(des.data.data.offset ?? 0) +
// @ts-ignore
des.x +
(fullSize ? fullWidth : width) / 2 -
boxSize.width / 2 -
NODE.width / 2 -
(width - fullWidth) / 2
: 0,
y: des.y || 0
}
}))
return newNodes
}
$: graph = graphBuilder(
modules,
{
disableAi,
insertable,
flowModuleStates,
selectedId: $selectedId
},
failureModule,
{
deleteBranch: (detail, label) => {
$selectedId = label
dispatch('deleteBranch', detail)
},
insert: (detail) => {
dispatch('insert', detail)
},
select: (modId) => {
if (!notSelectable) {
if ($selectedId != modId) {
$selectedId = modId
}
dispatch('select', modId)
}
},
delete: (detail, label) => {
$selectedId = label
dispatch('delete', detail)
},
newBranch: (module) => {
dispatch('newBranch', { module })
},
move: (module, modules) => {
dispatch('move', { module, modules })
},
selectedIteration: (detail, moduleId) => {
dispatch('selectedIteration', { ...detail, moduleId: moduleId })
}
},
success,
$useDataflow,
$selectedId,
moving
)
const nodes = writable<Node[]>([])
const edges = writable<Edge[]>([])
let height = 0
function updateStores() {
$nodes = layoutNodes(graph?.nodes)
$edges = graph.edges
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 40), minHeight)
}
$: graph && updateStores()
const nodeTypes = {
input2: InputNode,
module: ModuleNode,
branchAllStart: BranchAllStart,
branchAllEnd: BranchAllEndNode,
forLoopEnd: ForLoopEndNode,
forLoopStart: ForLoopStartNode,
result: ResultNode,
whileLoopStart: ForLoopStartNode,
whileLoopEnd: ForLoopEndNode,
branchOneStart: BranchOneStart,
branchOneEnd: BranchAllEndNode,
noBranch: NoBranchNode
} as any
const edgeTypes = {
edge: BaseEdge,
empty: EmptyEdge,
dataflowedge: DataflowEdge
} as any
const proOptions = { hideAttribution: true }
$: showDataflow =
$selectedId != undefined &&
!$selectedId.startsWith('constants') &&
!$selectedId.startsWith('settings') &&
$selectedId !== 'failure' &&
$selectedId !== 'Result'
const viewport = writable<Viewport>({
x: 0,
y: 5,
zoom: 1
})
function centerViewport(width: number) {
viewport.update((vp) => ({
...vp,
x: (width - fullWidth) / 2,
y: vp.y
}))
}
$: width && centerViewport(width)
</script>
<div style={`height: ${height}px; max-height: ${maxHeight}px;`} bind:clientWidth={width}>
<SvelteFlow
{nodes}
{edges}
{edgeTypes}
{nodeTypes}
{viewport}
{height}
minZoom={0.5}
connectionLineType={ConnectionLineType.SmoothStep}
defaultEdgeOptions={{ type: 'smoothstep' }}
preventScrolling={scroll}
zoomOnDoubleClick={false}
elementsSelectable={false}
{proOptions}
nodesDraggable={false}
>
<div class="absolute inset-0 !bg-surface-secondary" />
<Controls position="top-right" orientation="horizontal" showLock={false}>
{#if download}
<ControlButton
on:click={() => {
try {
localStorage.setItem('svelvet', encodeState({ modules, failureModule }))
} catch (e) {
console.error('error interacting with local storage', e)
}
window.open('/view_graph', '_blank')
}}
class="!bg-surface"
>
<Expand size="14" />
</ControlButton>
{/if}
</Controls>
<Controls
position="top-left"
orientation="horizontal"
showLock={false}
showZoom={false}
showFitView={false}
>
{#if showDataflow}
<Toggle
value={$useDataflow}
on:change={() => {
$useDataflow = !$useDataflow
}}
size="xs"
options={{
right: 'Dataflow'
}}
/>
{/if}
</Controls>
</SvelteFlow>
</div>
<style>
:global(.svelte-flow__handle) {
opacity: 0;
}
:global(.svelte-flow__controls-button) {
@apply bg-surface border-0;
}
:global(.svelte-flow__controls-button:hover) {
@apply bg-surface-hover;
}
:global(.svelte-flow__edgelabel-renderer) {
@apply z-50;
}
</style>
@@ -0,0 +1,453 @@
import type { FlowModule } from '$lib/gen'
import { type Node, type Edge } from '@xyflow/svelte'
import { getDependeeAndDependentComponents } from '../flows/flowExplorer'
import { dfsByModule } from '../flows/previousResults'
import { defaultIfEmptyString } from '$lib/utils'
export type GraphEventHandlers = {
insert: (detail) => void
deleteBranch: (detail, label: string) => void
select: (modId: string) => void
delete: (detail, label: string) => void
newBranch: (module: FlowModule) => void
move: (module: FlowModule, modules: FlowModule[]) => void
selectedIteration: (detail, moduleId: string) => void
}
export default function graphBuilder(
modules: FlowModule[] | undefined,
extra: Record<string, any>,
failureModule: FlowModule | undefined,
eventHandlers: GraphEventHandlers,
success: boolean | undefined,
useDataflow: boolean | undefined,
selectedId: string | undefined,
moving: string | undefined
): {
nodes: Node[]
edges: Edge[]
} {
const nodes: Node[] = []
const edges: Edge[] = []
if (!modules) {
return { nodes, edges }
}
function addNode(module: FlowModule, offset: number, type: string, subModules?: FlowModule[]) {
nodes.push({
id: module.id,
data: {
value: module.value,
offset: offset,
module: module,
modules: subModules ?? modules,
parentIds: [],
eventHandlers: eventHandlers,
moving: moving,
...extra
},
position: { x: -1, y: -1 },
type: type
})
return module.id
}
const parents: { [key: string]: string[] } = {}
function addEdge(
sourceId: string,
targetId: string,
options?: {
customId?: string
type?: string
subModules?: FlowModule[]
disableMoveIds?: string[]
}
) {
parents[targetId] = [...(parents[targetId] ?? []), sourceId]
// Find the index of the target module in the modules array
const mods = options?.subModules ?? modules
// Index of the target module in the modules array
let index = mods?.findIndex((m) => m.id === targetId) ?? -1
edges.push({
id: options?.customId || `edge:${sourceId}->${targetId}`,
source: sourceId,
target: targetId,
type: options?.type ?? 'edge',
data: {
insertable: extra.insertable,
modules: options?.subModules ?? modules,
sourceId,
targetId,
moving,
eventHandlers,
disableMoveIds: options?.disableMoveIds,
enableTrigger: sourceId === 'Input',
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
index: index >= 0 ? index : mods?.length ?? 0,
...extra
}
})
}
const inputNode: Node = {
id: 'Input',
position: { x: -1, y: -1 },
type: 'input2',
data: {
eventHandlers: eventHandlers,
modules: modules,
...extra
}
}
const resultNode: Node = {
id: 'result',
data: {
eventHandlers: eventHandlers,
modules: modules,
success: success,
...extra
},
position: { x: -1, y: -1 },
type: 'result'
}
nodes.push(inputNode)
nodes.push(resultNode)
function processModules(
modules: FlowModule[],
beforeNode: Node,
nextNode: Node,
currentOffset = 0,
disableMoveIds: string[] = []
) {
let previousId: string | undefined = undefined
if (modules.length === 0) {
addEdge(beforeNode.id, nextNode.id, {
subModules: modules,
disableMoveIds
})
} else {
modules.forEach((module, index) => {
const localDisableMoveIds = [...disableMoveIds, module.id]
// Add the edge between the previous node and the current one
if (index > 0 && previousId) {
addEdge(previousId, module.id, {
subModules: modules,
disableMoveIds
})
}
if (module.value.type === 'branchall') {
// Start
addNode(module, currentOffset, 'module', modules)
// "Collect result of each branch" node
const endNode = {
id: `${module.id}-end`,
data: {
offset: currentOffset,
id: module.id,
module: module,
modules: modules,
...extra
},
position: { x: -1, y: -1 },
type: 'branchAllEnd'
}
nodes.push(endNode)
if (module.value.branches.length === 0) {
// Add a "No branches" node
const startNode = {
id: `${module.id}-branch-0`,
data: {
offset: currentOffset,
id: module.id,
branchIndex: -1,
modules: modules,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'noBranch'
}
nodes.push(startNode)
addEdge(module.id, startNode.id, {
type: 'empty'
})
addEdge(startNode.id, endNode.id, {
type: 'empty'
})
} else {
module.value.branches.forEach((branch, branchIndex) => {
// Start node by branch
const startNode = {
id: `${module.id}-branch-${branchIndex}`,
data: {
offset: currentOffset,
label: defaultIfEmptyString(branch.summary, `Branch ${branchIndex + 1}`),
id: module.id,
branchIndex: branchIndex,
modules: modules,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'branchAllStart'
}
nodes.push(startNode)
addEdge(module.id, startNode.id, {
type: 'empty'
})
processModules(branch.modules, startNode, endNode, currentOffset, localDisableMoveIds)
})
}
previousId = endNode.id
} else if (module.value.type === 'forloopflow') {
addNode(module, currentOffset, 'module', modules)
const startNode = {
id: `${module.id}-start`,
data: {
offset: currentOffset + 25,
id: module.id,
module: module,
modules: modules,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'forLoopStart'
}
addEdge(module.id, startNode.id, {
type: 'empty'
})
const endNode = {
id: `${module.id}-end`,
data: {
offset: currentOffset,
id: module.id,
module: module,
modules: modules,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'forLoopEnd'
}
nodes.push(startNode)
nodes.push(endNode)
processModules(
module.value.modules,
startNode,
endNode,
currentOffset + 25,
localDisableMoveIds
)
previousId = endNode.id
} else if (module.value.type === 'whileloopflow') {
addNode(module, currentOffset, 'module', modules)
const startNode = {
id: `${module.id}-start`,
data: {
offset: currentOffset + 25,
module: module,
modules: modules,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'whileLoopStart'
}
addEdge(module.id, startNode.id, {
type: 'empty'
})
const endNode = {
id: `${module.id}-end`,
data: { offset: currentOffset, module: module, modules: modules, ...extra },
position: { x: -1, y: -1 },
type: 'whileLoopEnd'
}
nodes.push(startNode)
nodes.push(endNode)
processModules(
module.value.modules,
startNode,
endNode,
currentOffset + 25,
localDisableMoveIds
)
previousId = endNode.id
} else if (module.value.type === 'branchone') {
addNode(module, currentOffset, 'module', modules)
const endNode = {
id: `${module.id}-end`,
data: { offset: currentOffset, eventHandlers: eventHandlers },
position: { x: -1, y: -1 },
type: 'branchOneEnd'
}
nodes.push(endNode)
// Add default branch
const defaultBranch = {
id: `${module.id}-default`,
data: {
offset: currentOffset,
label: 'Default',
id: module.id,
branchIndex: -1,
modules: module.value.default,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'noBranch'
}
nodes.push(defaultBranch)
addEdge(module.id, defaultBranch.id, { type: 'empty' })
processModules(
module.value.default,
defaultBranch,
endNode,
currentOffset,
localDisableMoveIds
)
module.value.branches.forEach((branch, branchIndex) => {
// Start node by branch
const startNode = {
id: `${module.id}-branch-${branchIndex}`,
data: {
offset: currentOffset,
label:
defaultIfEmptyString(branch.summary, 'Branch ' + (branchIndex + 1)) +
'\n`' +
branch.expr +
'`',
id: module.id,
branchIndex: branchIndex,
modules: modules,
eventHandlers: eventHandlers,
...extra
},
position: { x: -1, y: -1 },
type: 'branchOneStart'
}
nodes.push(startNode)
addEdge(module.id, startNode.id, { type: 'empty' })
processModules(branch.modules, startNode, endNode, currentOffset, localDisableMoveIds)
})
previousId = endNode.id
} else {
addNode(module, currentOffset, 'module', modules)
previousId = module.id
}
if (index === 0) {
addEdge(beforeNode.id, module.id, {
subModules: modules,
disableMoveIds
})
}
if (index === modules.length - 1 && previousId) {
addEdge(previousId, nextNode.id, {
subModules: modules,
disableMoveIds
})
}
})
}
}
processModules(modules, inputNode, resultNode)
if (failureModule) {
addNode(failureModule, 0, 'module')
}
Object.keys(parents).forEach((key) => {
const node = nodes.find((n) => n.id === key)
if (node) {
node.data.parentIds = parents[key]
}
})
if (useDataflow && selectedId) {
let deps = getDependeeAndDependentComponents(selectedId, modules ?? [], failureModule)
if (deps) {
Object.entries(deps.dependees).forEach((x, i) => {
const inputs = x[1]
inputs?.forEach((input, index) => {
let pid = x[0]
if (input?.startsWith('flow_input.iter')) {
const parent = dfsByModule(selectedId!, modules ?? [])?.pop()
if (parent?.id) {
pid = parent.id
}
}
addEdge(pid, selectedId!, {
customId: `dep-${pid}-${selectedId}-${input}-${index}`,
type: 'dataflowedge'
})
})
})
Object.entries(deps.dependents).forEach((x, i) => {
let pid = x[0]
addEdge(selectedId!, pid, {
customId: `dep-${selectedId}-${pid}-${i}`,
type: 'dataflowedge'
})
})
}
}
return { nodes, edges }
}
@@ -1,4 +1,2 @@
export { default as FlowGraph } from './FlowGraph.svelte'
export * from './model'
export * from './util'
@@ -1,18 +1,9 @@
import type { FlowStatusModule, Job } from '$lib/gen'
import type { Writable } from 'svelte/store'
import type { UserNodeType } from './svelvet/types'
import type { FlowState } from '../flows/flowState'
export type ModuleHost = 'workspace' | 'inline' | 'hub'
export type Node = UserNodeType & {
parentIds: string[]
edgeLabel?: string
host?: ModuleHost
type: 'node'
loopDepth: number
}
export type Loop = {
type: 'loop'
items: NestedNodes
@@ -0,0 +1,131 @@
<script lang="ts">
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
import type { FlowModule } from '$lib/gen'
import { getBezierPath, BaseEdge, type Position, EdgeLabelRenderer } from '@xyflow/svelte'
import { ClipboardCopy } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { Writable } from 'svelte/store'
import type { GraphEventHandlers } from '../../graphBuilder'
import { getStraightLinePath } from '../utils'
import InsertTriggerButton from '$lib/components/flows/map/InsertTriggerButton.svelte'
import { twMerge } from 'tailwind-merge'
export let sourceX: number
export let sourceY: number
export let sourcePosition: Position
export let targetX: number
export let targetY: number
export let targetPosition: Position
export let markerEnd: string | undefined = undefined
export let data: {
insertable: boolean
modules: FlowModule[]
moving: string | undefined
eventHandlers: GraphEventHandlers
index: number
enableTrigger: boolean
disableAi: boolean
disableMoveIds: string[]
}
$: [edgePath] = getBezierPath({
sourceX,
sourceY: targetY - sourceY > 100 ? targetY - 100 : sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature: 0.25
})
$: completeEdge =
targetY - sourceY > 100
? `${edgePath} ${getStraightLinePath({ sourceX, sourceY, targetY })}`
: edgePath
const { useDataflow } = getContext<{
useDataflow: Writable<boolean | undefined>
}>('FlowGraphContext')
let menuOpen = false
</script>
<EdgeLabelRenderer>
{#if data?.insertable && !$useDataflow && !data?.moving}
<div
class={twMerge('edgeButtonContainer nodrag nopan top-0', menuOpen ? 'z-50' : '')}
style:transform="translate(-50%, 50%) translate({sourceX}px,{sourceY}px)"
>
<InsertModuleButton
disableAi={data.disableAi}
index={data.index ?? 0}
trigger={data.enableTrigger}
modules={data?.modules ?? []}
on:new={(e) => {
data?.eventHandlers.insert({ modules: data.modules, index: data.index, detail: e.detail })
}}
bind:open={menuOpen}
/>
</div>
{#if data.enableTrigger}
<div
class="edgeButtonContainer nodrag nopan"
style:transform="translate(100%, 50%) translate({sourceX}px,{sourceY}px)"
>
<InsertTriggerButton
disableAi={data.disableAi}
on:new={(e) => {
data?.eventHandlers.insert({
modules: data.modules,
index: data.index,
detail: e.detail
})
}}
index={data?.index ?? 0}
modules={data?.modules ?? []}
/>
</div>
{/if}
{/if}
{#if data?.moving}
<div
class="edgeButtonContainer nodrag nopan"
style:transform="translate(-50%, 50%) translate({sourceX}px,{sourceY}px)"
>
{#if data.moving && !data.disableMoveIds?.includes(data.moving)}
<button
title="Paste module"
on:click={() => {
data.eventHandlers.insert({
modules: data.modules,
index: data.index
})
}}
type="button"
class={twMerge(
'w-6 h-6 flex items-center justify-center',
'border border-gray-300 dark:border-gray-500',
'text-primary text-sm',
'bg-surface focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-surface-selected rounded-full '
)}
>
<ClipboardCopy size={14} />
</button>
{/if}
</div>
{/if}
</EdgeLabelRenderer>
<BaseEdge path={completeEdge} {markerEnd} class={$useDataflow ? 'hidden' : ''} />
<style>
.edgeButtonContainer {
position: absolute;
font-size: 12pt;
/* everything inside EdgeLabelRenderer has no pointer events by default */
/* if you have an interactive element, set pointer-events: all */
pointer-events: all;
}
</style>
@@ -0,0 +1,46 @@
<script lang="ts">
import type { FlowModule } from '$lib/gen'
import { getBezierPath, BaseEdge, type Position, EdgeLabelRenderer } from '@xyflow/svelte'
import { getStraightLinePath } from '../utils'
export let sourceX: number
export let sourceY: number
export let sourcePosition: Position
export let targetX: number
export let targetY: number
export let targetPosition: Position
export let markerEnd: string | undefined = undefined
export let data: {
modules: FlowModule[]
sourceId: string
targetId: string
}
$: [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY: targetY - sourceY > 100 ? targetY - 100 : sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature: 0.25
})
</script>
<EdgeLabelRenderer>
<div
class="absolute cursor-pointer nodrag nopan bg-surface-selected p-1 border text-xs"
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
>
{data.sourceId} -> {data.targetId}
</div>
</EdgeLabelRenderer>
<BaseEdge
path={targetY - sourceY > 100
? `${edgePath} ${getStraightLinePath({ sourceX, sourceY, targetY })}`
: edgePath}
{markerEnd}
style={`animation:dashdraw 0.5s linear infinite; stroke-dasharray: 5px;`}
/>
@@ -0,0 +1,29 @@
<script lang="ts">
import { getBezierPath, BaseEdge, type Position } from '@xyflow/svelte'
import { getContext } from 'svelte'
import type { Writable } from 'svelte/store'
export let sourceX: number
export let sourceY: number
export let sourcePosition: Position
export let targetX: number
export let targetY: number
export let targetPosition: Position
export let markerEnd: string | undefined = undefined
const { useDataflow } = getContext<{
useDataflow: Writable<boolean | undefined>
}>('FlowGraphContext')
$: [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature: 0.25
})
</script>
<BaseEdge path={edgePath} {markerEnd} class={$useDataflow ? 'hidden' : ''} />
@@ -0,0 +1,64 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import { X } from 'lucide-svelte'
import type { GraphModuleState } from '../../model'
import { getStateColor } from '../../util'
import type { FlowModule } from '$lib/gen'
import type { GraphEventHandlers } from '../../graphBuilder'
import { computeBorderStatus } from '../utils'
export let data: {
label: string
branchIndex: number
insertable: boolean
flowModuleStates: Record<string, GraphModuleState> | undefined
id: string
modules: FlowModule[]
selected: boolean
eventHandlers: GraphEventHandlers
offset: number
}
$: borderStatus = computeBorderStatus(
data.branchIndex,
'branchall',
data.flowModuleStates?.[data.id]
)
</script>
<NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem
label={data.label}
selectable
selected={data.selected}
bgColor={getStateColor(undefined, darkMode)}
borderColor={borderStatus
? getStateColor(borderStatus, darkMode) + (!darkMode ? '; border-width: 3px' : '')
: undefined}
on:select={() => {
data.eventHandlers.select(data.id)
}}
on:insert={(e) => {
data.eventHandlers.insert(e.detail)
}}
/>
{#if data.insertable}
<button
class="z-50 absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] center-center text-primary
border-[1.5px] border-gray-700 bg-surface duration-150 hover:bg-red-400 hover:text-white
hover:border-red-700"
on:click|preventDefault|stopPropagation={() => {
data.eventHandlers.deleteBranch(
{
module: data.modules.find((m) => m.id === data.id),
index: data.branchIndex
},
data.label
)
}}
>
<X size={12} />
</button>
{/if}
</NodeWrapper>
@@ -0,0 +1,61 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import { X } from 'lucide-svelte'
import type { GraphModuleState } from '../../model'
import { getStateColor } from '../../util'
import type { FlowModule } from '$lib/gen'
import type { GraphEventHandlers } from '../../graphBuilder'
import { computeBorderStatus } from '../utils'
export let data: {
label: string
insertable: boolean
flowModuleStates: Record<string, GraphModuleState> | undefined
id: string
modules: FlowModule[]
selected: boolean
eventHandlers: GraphEventHandlers
branchIndex: number
offset: number
}
$: borderStatus = computeBorderStatus(
data.branchIndex,
'branchone',
data.flowModuleStates?.[data.id]
)
</script>
<NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem
label={data.label}
selectable
selected={data.selected}
bgColor={getStateColor(undefined, darkMode)}
borderColor={borderStatus
? getStateColor(borderStatus, darkMode) + (!darkMode ? '; border-width: 3px' : '')
: undefined}
on:select={() => {
data.eventHandlers.select(data.id)
}}
/>
{#if data.insertable}
<button
class="z-50 absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] center-center text-primary
border-[1.5px] border-gray-700 bg-surface duration-150 hover:bg-red-400 hover:text-white
hover:border-red-700"
on:click|preventDefault|stopPropagation={() => {
data.eventHandlers.deleteBranch(
{
module: data.modules.find((m) => m.id === data.id),
index: data.branchIndex
},
data.label
)
}}
>
<X size={12} />
</button>
{/if}
</NodeWrapper>
@@ -0,0 +1,29 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { GraphModuleState } from '../../model'
import { getStateColor } from '../../util'
import type { GraphEventHandlers } from '../../graphBuilder'
export let data: {
offset: number
id: string
insertable: boolean
flowModuleStates: Record<string, GraphModuleState> | undefined
eventHandlers: GraphEventHandlers
}
</script>
<NodeWrapper offset={data.offset} let:darkMode>
<VirtualItem
label={'Collect result of each iteration'}
selectable={true}
selected={false}
id={data.id}
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,31 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { FlowModule } from '$lib/gen'
import { getStateColor } from '../../util'
import type { GraphModuleState } from '../../model'
import type { GraphEventHandlers } from '../../graphBuilder'
export let data: {
offset: number
id: string
modules: FlowModule[]
flowModuleStates: Record<string, GraphModuleState> | undefined
eventHandlers: GraphEventHandlers
}
</script>
<NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem
label={'Do one iteration'}
selectable={false}
selected={false}
id={data.id}
hideId
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,90 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import { ExternalLink, Wand2 } from 'lucide-svelte'
import NodeWrapper from './NodeWrapper.svelte'
import Popover from '$lib/components/Popover.svelte'
import type { GraphEventHandlers } from '../../graphBuilder'
import type { FlowModule } from '$lib/gen'
import { getStateColor } from '../../util'
import Menu from '$lib/components/common/menu/Menu.svelte'
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import { getContext } from 'svelte'
import { copilotInfo } from '$lib/stores'
import { base } from '$lib/base'
import type { Writable } from 'svelte/store'
export let data: {
insertable: boolean
eventHandlers: GraphEventHandlers
modules: FlowModule[]
disableAi: boolean
}
let openNoCopilot = false
const { drawerStore: copilotDrawerStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
const { selectedId } = getContext<{
selectedId: Writable<string | undefined>
}>('FlowGraphContext')
</script>
<NodeWrapper enableTargetHandle={false} let:darkMode>
{#if data.insertable && !data.disableAi}
<div class="absolute -top-10 left-1/2 transform -translate-x-1/2 z-10">
<Popover>
<div class={openNoCopilot ? 'z-10' : ''}>
<Menu pointerDown noMinW placement="bottom-center" let:close bind:show={openNoCopilot}>
<button
title="AI Flow Builder"
on:pointerdown={$copilotInfo.exists_openai_resource_path
? (ev) => {
ev.preventDefault()
ev.stopPropagation()
$copilotDrawerStore?.openDrawer()
}
: undefined}
slot="trigger"
type="button"
class=" bg-surface text-violet-800 dark:text-violet-400 border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-8 h-8 flex items-center justify-center"
>
<Wand2 size={16} />
</button>
{#if !$copilotInfo.exists_openai_resource_path}
<div class="text-primary p-4">
<p class="text-sm w-80">
Enable Windmill AI in the
<a
href="{base}/workspace_settings?tab=openai"
target="_blank"
class="inline-flex flex-row items-center gap-1"
on:click={() => {
close()
}}
>
workspace settings
<ExternalLink size={16} />
</a>
</p>
</div>
{/if}
</Menu>
</div>
<svelte:fragment slot="text">AI Flow builder</svelte:fragment>
</Popover>
</div>
{/if}
<VirtualItem
label="Input"
selectable
selected={$selectedId === 'Input'}
bgColor={getStateColor(undefined, darkMode)}
on:insert={(e) => {
data.eventHandlers?.insert(e.detail)
}}
on:select={(e) => {
data.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,98 @@
<script lang="ts">
import MapItem from '$lib/components/flows/map/MapItem.svelte'
import type { FlowModule, FlowModuleValue } from '$lib/gen'
import { GitBranchPlus } from 'lucide-svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { GraphEventHandlers } from '../../graphBuilder'
import type { GraphModuleState } from '../../model'
import { getStateColor } from '../../util'
export let data: {
offset: number
value: FlowModuleValue
module: FlowModule
insertable: boolean
insertableEnd: boolean
branchable: boolean
bgColor: string
modules: FlowModule[]
moving: string | undefined
disableAi: boolean
wrapperId: string | undefined
retries: number | undefined
flowJobs:
| { flowJobs: string[]; selected: number; flowJobsSuccess: (boolean | undefined)[] }
| undefined
eventHandlers: GraphEventHandlers
flowModuleStates: Record<string, GraphModuleState> | undefined
selected: boolean
}
$: type = data.flowModuleStates?.[data.module.id]?.type
if (!type && data.flowJobs) {
type = 'InProgress'
}
$: state = data.flowModuleStates?.[data.module.id]
$: flowJobs = state?.flow_jobs
? {
flowJobs: state?.flow_jobs,
selected: state?.selectedForloopIndex ?? -1,
flowJobsSuccess: state?.flow_jobs_success
}
: (undefined as any)
let selectedIteration = -1
</script>
<NodeWrapper offset={data.offset} let:darkMode>
<MapItem
mod={data.module}
insertable={data.insertable}
annotation={flowJobs &&
(data.module.value.type === 'forloopflow' || data.module.value.type === 'whileloopflow')
? 'Iteration: ' +
(selectedIteration >= 0 ? selectedIteration : state?.flow_jobs?.length) +
'/' +
(state?.iteration_total ?? '?')
: ''}
bgColor={getStateColor(type, darkMode, '#fff')}
modules={data.modules ?? []}
moving={data.moving}
duration_ms={state?.duration_ms}
retries={data.retries}
{flowJobs}
on:delete={(e) => {
data.eventHandlers.delete(e.detail, '')
}}
on:insert={(e) => {
data.eventHandlers.insert(e.detail)
}}
on:move={(e) => {
data.eventHandlers.move(data.module, data.modules)
}}
on:newBranch={(e) => {
data.eventHandlers.newBranch(data.module)
}}
on:select={(e) => {
data.eventHandlers.select(e.detail)
}}
on:selectedIteration={(e) => {
selectedIteration = e.detail.index + 1
data.eventHandlers.selectedIteration(e.detail, data.module.id)
}}
/>
<div class="absolute -bottom-10 left-1/2 transform -translate-x-1/2 z-10">
{#if (data.value.type === 'branchall' || data.value.type === 'branchone') && data.insertable}
<button
class="rounded-full border hover:bg-surface-hover bg-surface p-1"
on:click={() => {
data?.eventHandlers?.newBranch(data.module)
}}
>
<GitBranchPlus size={16} />
</button>
{/if}
</div>
</NodeWrapper>
@@ -0,0 +1,33 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { FlowModule } from '$lib/gen'
import type { GraphEventHandlers } from '../../graphBuilder'
import { getStateColor } from '../../util'
import type { GraphModuleState } from '../../model'
export let data: {
id: string
insertable: boolean
modules: FlowModule[]
eventHandlers: GraphEventHandlers
flowModuleStates: Record<string, GraphModuleState> | undefined
offset: number
label: string | undefined
}
</script>
<NodeWrapper offset={data.offset} let:darkMode enableSourceHandle enableTargetHandle>
<VirtualItem
label={data.label ?? 'No branches'}
id={data.id}
hideId={true}
selectable={true}
selected={false}
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data?.flowModuleStates?.[data?.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,24 @@
<script lang="ts">
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { Handle, Position } from '@xyflow/svelte'
export let enableSourceHandle: boolean = true
export let enableTargetHandle: boolean = true
export let offset: number = 0
let darkMode: boolean = false
</script>
<DarkModeObserver bind:darkMode />
<div class="relative shadow-md" style={`margin-left: ${offset}px;`}>
<slot {darkMode} />
</div>
{#if enableSourceHandle}
<Handle type="source" position={Position.Bottom} style={`margin-left: ${offset / 2}px;`} />
{/if}
{#if enableTargetHandle}
<Handle type="target" position={Position.Top} style={`margin-left: ${offset / 2}px;`} />
{/if}
@@ -0,0 +1,36 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { FlowModule } from '$lib/gen'
import type { GraphEventHandlers } from '../../graphBuilder'
import { getStateColor } from '../../util'
import type { Writable } from 'svelte/store'
import { getContext } from 'svelte'
export let data: {
eventHandlers: GraphEventHandlers
modules: FlowModule[]
success: boolean | undefined
}
const { selectedId } = getContext<{
selectedId: Writable<string | undefined>
}>('FlowGraphContext')
</script>
<NodeWrapper let:darkMode enableSourceHandle={false}>
<VirtualItem
id={'Result'}
label={'Result'}
selectable={true}
selected={$selectedId === 'Result'}
hideId={true}
bgColor={getStateColor(
data.success == undefined ? undefined : data.success ? 'Success' : 'Failure',
darkMode
)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,31 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { FlowModule } from '$lib/gen'
import type { GraphEventHandlers } from '../../graphBuilder'
import { getStateColor } from '../../util'
import type { GraphModuleState } from '../../model'
export let data: {
id: string
insertable: boolean
modules: FlowModule[]
eventHandlers: GraphEventHandlers
flowModuleStates: Record<string, GraphModuleState> | undefined
offset: number
}
</script>
<NodeWrapper offset={data.offset} let:darkMode enableSourceHandle enableTargetHandle>
<VirtualItem
label={'Collect result from all branches'}
id={data.id}
selectable={true}
selected={false}
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data?.flowModuleStates?.[data?.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,29 @@
<script lang="ts">
import VirtualItem from '$lib/components/flows/map/VirtualItem.svelte'
import NodeWrapper from './NodeWrapper.svelte'
import type { GraphEventHandlers } from '../../graphBuilder'
import { getStateColor } from '../../util'
import type { GraphModuleState } from '../../model'
export let data: {
id: string
insertable: boolean
eventHandlers: GraphEventHandlers
flowModuleStates: Record<string, GraphModuleState> | undefined
offset: number
}
</script>
<NodeWrapper let:darkMode offset={data.offset}>
<VirtualItem
label={'Collect result from all branches'}
id={data.id}
selectable={true}
selected={false}
bgColor={getStateColor(undefined, darkMode)}
borderColor={getStateColor(data?.flowModuleStates?.[data?.id]?.type, darkMode)}
on:select={(e) => {
data?.eventHandlers?.select(e.detail)
}}
/>
</NodeWrapper>
@@ -0,0 +1,32 @@
import type { FlowStatusModule } from '$lib/gen'
import type { GraphModuleState } from '../model'
export function getStraightLinePath({ sourceX, sourceY, targetY }) {
return `M${sourceX},${sourceY} L${sourceX},${targetY - 100}`
}
export function computeBorderStatus(
branchIndex: number,
type: 'branchone' | 'branchall',
graphModuleState: GraphModuleState | undefined
): FlowStatusModule['type'] | undefined {
if (type === 'branchone') {
const branchChosen = graphModuleState?.branchChosen
if (branchChosen === branchIndex) {
return graphModuleState?.type
}
} else {
let flow_jobs_success = graphModuleState?.flow_jobs_success
if (!flow_jobs_success) {
return 'WaitingForPriorSteps'
} else {
let status = flow_jobs_success?.[branchIndex]
if (status == undefined) {
return 'WaitingForExecutor'
} else {
return status ? 'Success' : 'Failure'
}
}
}
}
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 OSLabs Beta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -1,7 +0,0 @@
# Container
This folder contains two containers: Svelvet and Canvas.
## Svelvet.svelte
Svelvet.svelte is the top level component
@@ -1,91 +0,0 @@
/*
This file contains "middleware" functions that sanitize user input (UserNodeType and UserEdgeType). Put functions that
maintain consistency between previous
*/
import { bottomCb, leftCb, rightCb, topCb } from '../../edges/controllers/anchorCbUser'
import type { UserEdgeType, UserNodeType } from '../../types/types'
/**
* sanitizeUserNodesAndEdges will sanitize the data initially passed in to Svelvet component. For example, the node that user specified have an integar as its id but to instantiate a Node and be compatible with uuid we will need to convert the integar id to a string.
* @param userNodes The array of nodes that have a UserNodeType
* @param userEdges The array of edges that have a UserEdgeType
* @returns An object of sanitized userNodes and userEdges
*/
export function sanitizeUserNodesAndEdges(userNodes: UserNodeType[], userEdges: UserEdgeType[]) {
convertIdToString(userNodes)
convertEdgeIdsToString(userEdges)
convertAnchorPositionsToCallbacks(userNodes, userEdges)
setDefaultEdgeType(userEdges)
return { userNodes, userEdges }
}
/**
* setDefaultEdgeType ensures a default edge type of 'bezier' if the user does not set one, or sets a nonsense type.
* @param userEdges The array of edges that have a UserEdgeType
* @returns No return, the function edits the objects in place
*/
function setDefaultEdgeType(userEdges) {
for (let userEdge of userEdges) {
if (!['smoothstep', 'step', 'bezier', 'straight'].includes(userEdge.type))
userEdge.type = 'bezier'
}
}
/**
* convertAnchorPositionsToCallbacks
* @description
* WHY: This function is in order to maintain compliance with earlier versions of Svelvet.
* HISTORY: In Svelvet<=5, anchor points were hard-coded onto each node. Each node had a "sourcePosition"
* "targetPosition" where the edges would be attached. In Svelvet6, the store was re-designed
* from the ground up into an object-relational data model where anchor points could be attached
* at any point on the node using callbacks. This enabled features such as custom edge position,
* adaptive edge positioning, and dynamic edges.
* The purpose of this function is to parse the old way of specifying edge positions (as two source/target
* anchors on the node) into the Svelvet6 (where anchors are separate objects) in order to maintain a
* consistent user experience. However, we suggest that this functionality (parsing Svelvet5 syntax into
* Svelvet6 syntax) be removed completely in favor of only using Svelvet6 syntax in order to reduce edge cases.
*/
function convertAnchorPositionsToCallbacks(userNodes: UserNodeType[], userEdges: UserEdgeType[]) {
// convert userNodes array into object for constant time lookup by id
const userNodesObj: { [key: string]: UserNodeType } = {}
for (let userNode of userNodes) userNodesObj[userNode.id] = userNode
// iterate through userEdges, and check the source/target nodes.
for (let userEdge of userEdges) {
const userNodeSource = userNodesObj[userEdge.source]
const userNodeTarget = userNodesObj[userEdge.target]
if (!userNodeSource || !userNodeTarget) continue
const sourcePosition = userNodeSource.sourcePosition
const targetPosition = userNodeTarget.targetPosition
const cbs = { left: leftCb, right: rightCb, top: topCb, bottom: bottomCb }
if (sourcePosition) userEdge.sourceAnchorCb = cbs[sourcePosition]
if (targetPosition) userEdge.targetAnchorCb = cbs[targetPosition]
}
}
/**
* Converts node id's to strings. For Svelvet<6, node id's were numbers. These were switched to strings for compatibility with uuid. This function does not return a new array, instead it mutates and sanitizes the original array.
* @param userNodes The array of userNodes (not yet sanitized)
*/
function convertIdToString(userNodes: UserNodeType[]) {
userNodes = userNodes.map((node) => {
node.id = node.id.toString()
node.childNodes =
node.childNodes === undefined ? [] : node.childNodes.map((childId) => childId.toString())
return node
})
}
/**
* Converts source/target node id's to string. For Svelvet<6, id's were numbers. These were switched to strings for compatibility with uuid. This function does not return a new array, instead it mutates and sanitizes the original array.
* @param userEdges The array of userEdges (not yet sanitized)
*/
function convertEdgeIdsToString(userEdges: UserEdgeType[]) {
userEdges = userEdges.map((edge) => {
edge.source = edge.source.toString()
edge.target = edge.target.toString()
return edge
})
}
@@ -1,304 +0,0 @@
<script lang="ts">
import { pointer, select, selectAll } from 'd3-selection'
import { zoom, zoomIdentity, zoomTransform } from 'd3-zoom'
import { createEventDispatcher, getContext, onMount } from 'svelte'
import SimpleBezierEdge from '../../edges/views/Edges/SimpleBezierEdge.svelte'
import SmoothStepEdge from '../../edges/views/Edges/SmoothStepEdge.svelte'
import StepEdge from '../../edges/views/Edges/StepEdge.svelte'
import Node from '../../nodes/views/Node.svelte'
import { determineD3Instance } from '../..//d3/controllers/d3'
import { findStore } from '../../store/models/store'
import { Expand, Minus, Plus } from 'lucide-svelte'
import Toggle from '$lib/components/Toggle.svelte'
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import { fade } from 'svelte/transition'
//these are typscripted as any, however they have been transformed inside of store.ts
export let canvasId: string
export let width: number
export let height: number
export let boundary = false
export let scroll = false
export let dataflow = false
export let download = false
export let showDataflowToggle: boolean = false
// here we lookup the store using the unique key
const store = findStore(canvasId)
const {
edgesStore,
nodesStore,
nodeSelected,
backgroundStore,
movementStore,
widthStore,
heightStore,
d3Scale
} = store
$: nodes = Object.values($nodesStore)
$: edges = Object.values($edgesStore)
// declaring the grid and dot size for d3's transformations and zoom
const gridSize = 15
const dotSize = 10
const dispatch = createEventDispatcher()
// leveraging d3 library to zoom/pan
let d3 = {
zoom,
zoomTransform,
zoomIdentity,
select,
selectAll,
pointer
}
let d3Zoom = determineD3Instance(
boundary,
d3,
nodeSelected,
width,
height,
movementStore,
backgroundStore,
gridSize,
dotSize,
canvasId,
d3Scale,
handleZoom
)
// d3Translate is used for the minimap
onMount(() => {
// actualizes the d3 instance
const nodes = d3.select(`#zoomable-${canvasId}`).call(d3Zoom).on('dblclick.zoom', null)
if (!scroll) {
;[nodes].forEach((d3Instance) => {
d3Instance
.on('wheel.zoom', null)
.on('mousewheel.zoom', null)
.on('mousemove.zoom', null)
.on('DOMMouseScroll.zoom', null)
})
}
d3.select(`#zoom_in_${canvasId}`).on('click', function () {
try {
// @ts-ignore
d3Zoom.scaleBy(nodes.transition().duration(250), 1.4)
} catch (e) {
console.log('error', e)
}
})
d3.select(`#zoom_out_${canvasId}`).on('click', function () {
try {
// @ts-ignore
d3Zoom.scaleBy(nodes.transition().duration(250), 0.714)
} catch (e) {
console.log('error', e)
}
})
})
function handleZoom(e) {
if (!$movementStore) return
//add a store that contains the current value of the d3-zoom's scale to be used in onMouseMove function
d3Scale.set(e.transform.k)
// transform 'g' SVG elements (edge, edge text, edge anchor)
d3.select(`.Edges-${canvasId} g`).attr('transform', e.transform)
// transform div elements (nodes)
//@ts-ignore
let transform = d3.zoomTransform(this)
store.d3ZoomParameters.set({ ...transform }) // record x,y position of pan, and zoom level
// selects and transforms all node divs from class 'Node' and performs transformation
d3.select(`.Node-${canvasId}`)
.style(
'transform',
'translate(' + transform.x + 'px,' + transform.y + 'px) scale(' + transform.k + ')'
)
.style('transform-origin', '0 0')
}
const { currentStepStore: copilotCurrentStepStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
$: {
if ($copilotCurrentStepStore !== undefined) {
// reset zoom
d3.select(`#zoomable-${canvasId}`).call(d3Zoom.transform, d3.zoomIdentity)
}
}
</script>
<div class="zoomable" id={`zoomable-${canvasId}`}>
<!-- This is the container that holds GraphView and we have disabled right click functionality to prevent a sticking behavior -->
<div id="graphview-container">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class={`Nodes Nodes-${canvasId}`} on:contextmenu|preventDefault>
<!-- This container is transformed by d3zoom -->
<div class={`Node Node-${canvasId}`}>
{#if $copilotCurrentStepStore !== undefined}
<div transition:fade class="fixed inset-0 bg-gray-500 bg-opacity-75 z-[900] !m-0" />
{/if}
{#each nodes as node}
{#if node.data.html}
<Node {node} {canvasId}>{@html node.data.html}</Node>
{:else if node.data.custom}
<Node isCustom {node} {canvasId}>
<svelte:component
this={node.data.custom.component}
on:new={(e) => node?.data?.custom?.cb?.('new', e.detail)}
on:delete={(e) => node?.data?.custom?.cb?.('delete', e.detail)}
on:select={(e) => node?.data?.custom?.cb?.('select', e.detail)}
on:insert={(e) => node?.data?.custom?.cb?.('insert', e.detail)}
on:newBranch={(e) => node?.data?.custom?.cb?.('newBranch', e.detail)}
on:deleteBranch={(e) => node?.data?.custom?.cb?.('deleteBranch', e.detail)}
on:move={(e) => node?.data?.custom?.cb?.('move', e.detail)}
on:nodeInsert={(e) => node?.data?.custom?.cb?.('nodeInsert', e.detail)}
on:addBranch={(e) => node?.data?.custom?.cb?.('addBranch', e.detail)}
on:removeBranch={(e) => node?.data?.custom?.cb?.('removeBranch', e.detail)}
on:selectedIteration={(e) =>
node?.data?.custom?.cb?.('selectedIteration', e.detail)}
{...node.data.custom.props}
/>
</Node>
{:else}
<Node {node} {canvasId}>{node.data.label}</Node>
{/if}
{/each}
</div>
</div>
</div>
<!-- rendering dots on the background depending on the zoom level -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<svg
class={`Edges Edges-${canvasId}`}
viewBox="0 0 {$widthStore} {$heightStore}"
on:contextmenu|preventDefault
>
<defs>
<pattern
id={`background-${canvasId}`}
x="0"
y="0"
width={gridSize}
height={gridSize}
patternUnits="userSpaceOnUse"
>
<circle
id="dot"
cx={gridSize / 2 - dotSize / 2}
cy={gridSize / 2 - dotSize / 2}
r="0.5"
style="fill: gray"
/>
</pattern>
</defs>
<!-- <g> tag defines which edge type to render depending on properties of edge object -->
<g>
{#each edges as edge}
{#if edge.type === 'smoothstep'}
<SmoothStepEdge {edge} {canvasId} />
{:else if edge.type === 'step'}
<StepEdge {edge} {canvasId} />
{:else}
<SimpleBezierEdge edgeId={edge.id} {canvasId} />
{/if}
{/each}
<!-- {#each filteredAnchors as anchor} -->
<!-- note that these are SVG -->
<!-- <EdgeAnchor x={anchor.positionX} y={anchor.positionY} /> -->
<!-- {/each} -->
</g>
</svg>
</div>
{#if showDataflowToggle}
<div id="dataflow_toggle">
<Toggle
disabled={$copilotCurrentStepStore !== undefined}
textClass="!text-secondary"
size="xs"
bind:checked={dataflow}
options={{
right: 'dataflow'
}}
/>
</div>
{/if}
<div id="buttons">
<button
title="Zoom In"
id={`zoom_in_${canvasId}`}
class={'relative overflow-hidden ' +
($copilotCurrentStepStore !== undefined
? '!bg-gray-400/20 text-black/20 border-gray-400/20'
: '')}
>
<Plus size="14" class="flex justify-start m-1" />
</button>
<button
title="Zoom Out"
id={`zoom_out_${canvasId}`}
class={'relative overflow-hidden ' +
($copilotCurrentStepStore !== undefined
? '!bg-gray-400/20 text-black/20 border-gray-400/20'
: '')}
>
<Minus size="14" class="flex justify-start m-1" />
</button>
{#if download}
<button on:click={() => dispatch('expand')}>
<Expand size="14" class="flex justify-start m-1" />
</button>
{/if}
</div>
<style>
#dataflow_toggle {
position: absolute;
top: 3px;
left: 8px;
}
#buttons {
position: absolute;
top: 8px;
right: 8px;
z-index: 20;
}
#buttons > button {
border-radius: 4px;
@apply bg-surface;
@apply border;
transition: all;
}
.Nodes {
position: absolute;
width: 100%;
height: 100%;
}
.Node {
color: black; /* remove this once color is set to default via types */
width: 100%;
height: 100%;
}
#graphview-container {
pointer-events: none;
}
.pointer-events-auto {
pointer-events: auto;
}
</style>
@@ -1,131 +0,0 @@
<script lang="ts">
const pkStringGenerator = () => (Math.random() + 1).toString(36).substring(7)
import type { UserEdgeType, UserNodeType } from '../../types/types'
import {
createStoreEmpty,
populateSvelvetStoreFromUserInput
} from '../../store/controllers/storeApi'
import { afterUpdate, onMount } from 'svelte'
import GraphView from './GraphView.svelte'
import { sanitizeUserNodesAndEdges } from '../controllers/middleware'
export let nodes: UserNodeType[]
export let edges: UserEdgeType[]
export let width: number = 600
export let height: number = 600
export let background: boolean = true
export let movement: boolean = true
export let canvasId: string = pkStringGenerator()
export let snap: boolean = false
export let snapTo: number = 30
export let nodeCreate: boolean = false
export let boundary = false
export let locked: boolean = false // if true, node movement is disabled
export let editable: boolean = false
export let highlightEdges: boolean = true
export let scroll: boolean = false
export let download: boolean = false
export let dataflow: boolean = false
export let nodeSelected: boolean = false
// generates a unique string for each svelvet component's unique store instance
// creates a store that uses the unique sting as the key to create and look up the corresponding store
// this way we can have multiple Svelvet Components on the same page and prevent overlap of information
const store = createStoreEmpty(canvasId)
// stores (state) within stores, so that we cannot access values from everywhere
// const { widthStore, heightStore, nodesStore, derivedEdges } = svelvetStore;
let error = ''
// sets the state of the store to the values passed in from the Svelvet Component on initial render
onMount(() => {
try {
// sanitize user input
let output = sanitizeUserNodesAndEdges(nodes, edges)
const userNodes = output['userNodes']
const userEdges = output['userEdges']
// set canvas related stores. you need to do this before setting node/edge related stores because
// initializing nodes/edges might read relevant options from the store.
store.widthStore.set(width)
store.heightStore.set(height)
store.backgroundStore.set(background)
store.movementStore.set(movement)
const optionsObj = { snap, snapTo } // TODO: rename to snap
store.options.set(optionsObj) //
store.nodeCreate.set(nodeCreate)
store.boundary.set(boundary)
store.lockedOption.set(locked)
store.editableOption.set(editable)
store.highlightEdgesOption.set(highlightEdges)
// set node/edge related stores
populateSvelvetStoreFromUserInput(canvasId, userNodes, userEdges)
error = ''
} catch (e) {
error = 'There was an error displaying the flow. Please report the error.' + e.message
console.error(e)
}
})
// // enables data reactivity. TODO: this needs to be added back in
// Probably need to use findStore, not create store
afterUpdate(() => {
try {
// sanitize user input
let output = sanitizeUserNodesAndEdges(nodes, edges)
const userNodes = output['userNodes']
const userEdges = output['userEdges']
// set canvas related stores. you need to do this before setting node/edge related stores because
// initializing nodes/edges might read relevant options from the store.
store.widthStore.set(width)
store.heightStore.set(height)
store.backgroundStore.set(background)
store.movementStore.set(movement)
const optionsObj = { snap, snapTo } // TODO: rename to snap
store.options.set(optionsObj) //
store.nodeCreate.set(nodeCreate)
store.boundary.set(boundary)
store.lockedOption.set(locked)
store.editableOption.set(editable)
store.highlightEdgesOption.set(highlightEdges)
// set node/edge related stores
populateSvelvetStoreFromUserInput(canvasId, userNodes, userEdges)
error = ''
} catch (e) {
error = 'There was an error displaying the flow. Please report the error.' + e.message
console.error(e)
}
})
</script>
<!-- Now that a store has been created from the initial nodes and initial edges we drill props from the store down to the D3 GraphView along with the unique key -->
<div
class="Svelvet bg-surface-secondary"
style={`width: ${width}px; min-height: 100%; height: ${height}px;`}
>
{#if error != ''}
<div class="error text-red-600 center-center p-4">{error}</div>
{:else}
<GraphView
showDataflowToggle={nodeSelected}
bind:dataflow
on:expand
{download}
{scroll}
{canvasId}
{width}
{height}
{boundary}
/>
{/if}
</div>
<style>
.Svelvet {
position: relative;
overflow: hidden;
display: grid;
font-family: 'Segoe UI', sans-serif;
z-index: 1;
}
</style>
@@ -1,50 +0,0 @@
// Required for "custom css" feature
import type { NodeType, StoreType } from '../../store/types/types'
// WHAT: For a given node with a user-defined classname, see if there are any css properties for height and width
// Get that height and width and set properties in node store. This is necessary for
export const forceCssHeightAndWidth = (store: StoreType, node: NodeType) => {
let width, height, innerText
// Look through each CSS rule to find the one the user defined
for (let i = 0; i < document.styleSheets.length; i++) {
const styleSheet = document.styleSheets[i]
const styleRules = styleSheet.cssRules
for (let j = 0; j < styleRules.length; j++) {
const rule = styleRules[j]
// this is necessary to get rid of typescript warning for rule.selectorText
if (!(rule instanceof CSSStyleRule)) continue
if (rule.selectorText === `.${node.className}`) {
const initialText = rule.cssText // getting the full text of the CSS rule
const i = initialText.indexOf('{') // finding index of first bracket
innerText = initialText.substring(i + 1, initialText.length - 1) // extracting the CSS to insert into inline style
// Adjusting the width and height if they are set via the custom class
const arr = innerText.split(' ')
arr.forEach((str, i) => {
if (str === 'width:') {
width = str.concat(arr[i + 1]) // go through the array and join width and the number
const w = parseInt(arr[i + 1]) // getting the number for the width
width = w
}
if (str === 'height:') {
height = str.concat(arr[i + 1]) // same as with the width
const h = parseInt(arr[i + 1])
height = h
}
})
}
}
}
// update the width/height of the node
store.nodesStore.update((nodes) => {
if (width !== undefined) nodes[node.id].width = width
if (height !== undefined) nodes[node.id].height = height
return { ...nodes }
})
// move the node a bit to force update to anchors, potential anchors
node.setPositionFromMovement(0, 0)
}
@@ -1,91 +0,0 @@
// import { zoom, zoomTransform } from 'd3-zoom';
// import { select, selectAll } from 'd3-selection';
import type { ZoomBehavior } from 'd3-zoom';
import { get } from 'svelte/store';
export function zoomInit(
d3,
canvasId,
d3Zoom,
d3Translate,
initialLocation,
initialZoom,
d3Scale
) {
//set default zoom logic
d3.select(`.Edges-${canvasId}`)
//makes sure translation is default at center coordinates
.transition()
.duration(0)
.call(d3Zoom.translateTo, 0, 0)
//moves camera to coordinates
.transition()
.duration(0)
.call(
d3Zoom.translateTo,
initialLocation.x / initialZoom,
initialLocation.y / initialZoom
)
// zooms in on selected point
.transition()
.duration(0)
.call(d3Zoom.scaleTo, initialZoom.toFixed(2));
// updates d3Translate with d3 object with x, y, and k values to be sent down to the minimap to be further calculated further
d3Translate = d3.zoomIdentity
.translate(initialLocation.x, initialLocation.y)
.scale(initialZoom.toFixed(2));
d3.select(`.Nodes-${canvasId}`)
.transition()
.duration(0)
.call(d3Zoom.translateTo, 0, 0)
.transition()
.duration(0)
.call(
d3Zoom.translateTo,
initialLocation.x / initialZoom,
initialLocation.y / initialZoom
)
.transition()
.duration(0)
.call(d3Zoom.scaleTo, initialZoom.toFixed(2));
// sets D3 scale to current k of object
d3Scale.set(d3.zoomTransform(d3.select(`.Nodes-${canvasId}`)).k);
return d3Translate;
}
// create d3 instance conditionally based on boundary prop
export function determineD3Instance(
boundary,
d3: { zoom: () => ZoomBehavior<any, any> },
nodeSelected,
width,
height,
movementStore,
backgroundStore,
gridSize,
dotSize,
canvasId,
d3Scale,
handleZoom
): ZoomBehavior<any, any> {
if (boundary) {
return d3
.zoom()
.filter(() => !get(nodeSelected))
.scaleExtent([0.4, 4]) // limits for zooming in/out
.translateExtent([
[0, 0],
[boundary.x, boundary.y],
]) // world extent
.extent([
[0, 0],
[width, height],
])
.on('zoom', handleZoom);
} else {
return d3
.zoom()
.filter(() => !get(nodeSelected))
.scaleExtent([0.4, 2])
.on('zoom', handleZoom);
}
}
@@ -1,119 +0,0 @@
import { rightCb, leftCb, topCb, bottomCb } from './anchorCbUser' // these are callbacks used to calculate anchor position relative to node
import type { AnchorCbType } from '../../edges/types/types'
import type { StoreType } from '../../store/types/types'
import { getAnchors } from './util'
import { getNodeById } from '../../nodes/controllers/util'
export function fixedCbCreator(
store: StoreType,
edgeId: string,
anchorId: string,
userNodeId: string,
positionCb: Function // positionCb should be a function that takes 4 arguments (x,y,width,height) and returns a 3-array [x,y,angle] that represents the x,y position of the anchor as well as it's angle with respect to it's node.
) {
const rn = <AnchorCbType>fixedCb
rn.type = 'fixed'
return rn
function fixedCb() {
// get the two anchors
const anchors = getAnchors(store, { edgeId: edgeId })
if (anchors.length !== 2) throw `there should be two anchors per edge, have: ${anchors.length}`
let [anchorSelf, anchorOther] = anchors
if (anchorSelf.id !== anchorId) [anchorSelf, anchorOther] = [anchorOther, anchorSelf]
const node = getNodeById(store, userNodeId)
const { positionX, positionY, width, height } = node
const [x, y, angle] = positionCb(positionX, positionY, width, height)
anchorSelf.positionX = x
anchorSelf.positionY = y
anchorSelf.angle = angle
// update the other anchor if it is a dynamic anchor
// Note dyanamic anchor callbacks have a check that prevents an infinite loop
if (anchorOther.callback.type === 'dynamic') anchorOther.callback()
}
}
export function dynamicCbCreator(store: StoreType, edgeId: string, anchorId: string) {
const rn = <AnchorCbType>dynamicCb
rn.type = 'dynamic'
return rn
function dynamicCb() {
// get the two anchors
const anchors = getAnchors(store, { edgeId: edgeId })
if (anchors.length !== 2) throw `there should be two anchors per edge, have: ${anchors.length}`
let [anchorSelf, anchorOther] = anchors
if (anchorSelf.id !== anchorId) [anchorSelf, anchorOther] = [anchorOther, anchorSelf]
// get the two nodes
const nodeSelf = getNodeById(store, anchorSelf.nodeId)
const nodeOther = getNodeById(store, anchorOther.nodeId)
// get the midpoints
const [xSelf, ySelf, xOther, yOther] = [
nodeSelf.positionX + nodeSelf.width / 2,
nodeSelf.positionY + nodeSelf.height / 2,
nodeOther.positionX + nodeOther.width / 2,
nodeOther.positionY + nodeOther.height / 2
]
// record angle for later. We use this so we don't have an infinite loop
let prevAngle = anchorSelf.angle
// calculate the slope
const slope = (ySelf - yOther) / (xSelf - xOther)
// slope<1 means -45 to 45 degrees so left/right anchors
if (Math.abs(slope) < 1) {
// self node is on the left, other node is on the right
if (nodeSelf.positionX < nodeOther.positionX) {
const [selfX, selfY] = rightCb(
nodeSelf.positionX,
nodeSelf.positionY,
nodeSelf.width,
nodeSelf.height
)
leftCb(nodeOther.positionX, nodeOther.positionY, nodeOther.width, nodeOther.height)
anchorSelf.setPosition(selfX, selfY)
anchorSelf.angle = 0 // if the self node is on the left, the anchor should have orientation of 0 degrees on the unit circle
} else {
// in this case, the self node is on the right and the other node is on the left
const [selfX, selfY] = leftCb(
nodeSelf.positionX,
nodeSelf.positionY,
nodeSelf.width,
nodeSelf.height
)
rightCb(nodeOther.positionX, nodeOther.positionY, nodeOther.width, nodeOther.height)
anchorSelf.setPosition(selfX, selfY)
anchorSelf.angle = 180
}
} else {
if (nodeSelf.positionY < nodeOther.positionY) {
// here the self node is above the other node
const [selfX, selfY] = bottomCb(
nodeSelf.positionX,
nodeSelf.positionY,
nodeSelf.width,
nodeSelf.height
)
topCb(nodeOther.positionX, nodeOther.positionY, nodeOther.width, nodeOther.height)
anchorSelf.setPosition(selfX, selfY)
anchorSelf.angle = 270
} else {
const [selfX, selfY] = topCb(
nodeSelf.positionX,
nodeSelf.positionY,
nodeSelf.width,
nodeSelf.height
)
bottomCb(nodeOther.positionX, nodeOther.positionY, nodeOther.width, nodeOther.height)
anchorSelf.setPosition(selfX, selfY)
anchorSelf.angle = 90
}
}
// if the anchor changed position, then do operation for other anchor
// otherwise, don't do anything. This check is so we don't have an infinite loop
if (prevAngle !== anchorSelf.angle) anchorOther.callback()
}
}
@@ -1,98 +0,0 @@
/**
* These are callbacks used to define anchor positions relative to the node they are attached to.
* These may be provided to developers as examples of how to write their own custom callbacks for adjustable anchors
* It calculates the position of an anchor (x,y) coordinates given a node parameterized by (x,y,width, height)
*/
/**
* @function rightCb - This is a callback function to define the anchor position on the node to be on the right side of the node.
* @param xNode - positionX of the attached node
* @param yNode - positionY of the attached node
* @param widthNode - width of the attached node
* @param heightNode - height of the attached node
* @returns [xAnchor, yAnchor, 0]
* xAnchor - positionX for the anchor
* yAnchor - positionY for the anchor
* 0 - this is angle of the anchor with respect to the node. The right anchor should fall at 0 degrees on the unit circle.
* @export rightCb
*/
export const rightCb = (
xNode: number,
yNode: number,
widthNode: number,
heightNode: number
) => {
const xAnchor = xNode + widthNode;
const yAnchor = yNode + heightNode / 2;
return [xAnchor, yAnchor, 0];
};
/**
* @function leftCb - This is a callback function to define the anchor position on the node to be on the left side of the node.
* @param xNode - positionX of the attached node
* @param yNode - positionY of the attached node
* @param widthNode - width of the attached node
* @param heightNode - height of the attached node
* @returns [xAnchor, yAnchor, 180]
* xAnchor - positionX for the anchor
* yAnchor - positionY for the anchor
* 180 - this is angle of the anchor with respect to the node. The left anchor should fall at 180 degrees on the unit circle.
* @export leftCb
*/
export const leftCb = (
xNode: number,
yNode: number,
widthNode: number,
heightNode: number
) => {
const xAnchor = xNode;
const yAnchor = yNode + heightNode / 2;
return [xAnchor, yAnchor, 180];
};
/**
* @function topCb - This is a callback function to define the anchor position on the node to be on the top of the node.
* @param xNode - positionX of the attached node
* @param yNode - positionY of the attached node
* @param widthNode - width of the attached node
* @param heightNode - height of the attached node
* @returns [xAnchor, yAnchor, 90]
* xAnchor - positionX for the anchor
* yAnchor - positionY for the anchor
* 90 - this is angle of the anchor with respect to the node. The top anchor should fall at 90 degrees on the unit circle.
* @export topCb
*/
export const topCb = (
xNode: number,
yNode: number,
widthNode: number,
heightNode: number
) => {
const xAnchor = xNode + widthNode / 2;
const yAnchor = yNode;
return [xAnchor, yAnchor, 90];
};
/**
* @function bottomCb - This is a callback function to define the anchor position on the node to be on the bottom of the node.
* @param xNode - positionX of the attached node
* @param yNode - positionY of the attached node
* @param widthNode - width of the attached node
* @param heightNode - height of the attached node
* @returns - [xAnchor, yAnchor, 90]
* xAnchor - positionX for the anchor
* yAnchor - positionY for the anchor
* 270 - this is angle of the anchor with respect to the node. The bottom anchor should fall at 270 degrees on the unit circle.
* @export bottomCb
*/
export const bottomCb = (
xNode: number,
yNode: number,
widthNode: number,
heightNode: number
) => {
const xAnchor = xNode + widthNode / 2;
const yAnchor = yNode + heightNode;
return [xAnchor, yAnchor, 270];
};
@@ -1,78 +0,0 @@
import type { StoreType } from '../../store/types/types'
import { get } from 'svelte/store'
import type { AnchorType } from '../types/types'
/**
* Finds all Anchors that matches the conditions specified in the filter parameter from a Svelvet store and returns these Anchors in an array.
*
* @param store The Svelvet store containing the state of a Svelvet component
* @param filter An object to specify conditions. Example: if filter = \{ sourceOrTarget: 'source', positionX: 35 \} then we will return all anchors with sourceOrTarget = source AND poxitionX = 35
* @returns An array of Anchors that matches the conditions specified in the filter parameter
*/
export function getAnchors(store: StoreType, filter?: { [key: string]: any }) {
let anchors = Object.values(get(store.anchorsStore))
// filter the array of anchors for elements that match filter
// Example: if filter = {sourceOrTarget: 'source', positionX: 35} then we will
//return all anchors with sourceOrTarget = source AND poxitionX = 35
if (filter !== undefined) {
anchors = anchors.filter((anchor) => {
for (let filterKey in filter) {
const filterValue = filter[filterKey]
if (anchor[filterKey as keyof AnchorType] !== filterValue) return false
}
return true
})
}
// return list of anchors
return anchors
}
/**
* Gets one anchor (source anchor or target anchor) from a given edge
*
* @param store The Svelvet store containing the state of the Svelvet component
* @param edgeId The id of a given edge
* @param sourceOrTarget A string of 'source' or 'target' to specify which anchor the function should return
* @returns The source or target Anchor object of a given edge
*/
export function getAnchorFromEdge(
store: StoreType,
edgeId: string,
sourceOrTarget: 'source' | 'target'
): AnchorType {
getEdgeById(store, edgeId)
const anchors = getAnchors(store, { edgeId: edgeId })
if (anchors.length !== 2)
throw `there should be two anchors for a given edge, there are ${anchors.length}`
// there should be one source anchor and one target anchor. Return the source anchor
const anchor = anchors.filter((anchor) => anchor.sourceOrTarget === sourceOrTarget)
if (anchor.length !== 1) throw `there should only be one source/target anchor`
return anchor[0]
}
/**
* getEdgeById will look for the targeted Edge that has the same id provided in the Svelvet component store.
*
* @param store The Svelvet store containing the state of a Svelvet component
* @param id The id of the targeted Node
* @returns The targeted Edge object in store.edgesStore
*/
export function getEdgeById(store: StoreType, id: string) {
const edgesStore = get(store.edgesStore)
const edge = edgesStore[id]
if (edge === undefined) throw 'edge not found'
return edge
}
/**
* getAnchorById will look for the targeted Anchor that has the same id in the Svelvet component store.
*
* @param store The Svelvet store containing the state of a Svelvet component
* @param id The id of the targeted Anchor
* @returns The target Anchor object in store.anchorsStore
*/
export function getAnchorById(store: StoreType, id: string) {
const anchorsStore = get(store.anchorsStore)
const anchor = anchorsStore[id]
return anchor
}
@@ -1,119 +0,0 @@
/**
* This is where we create, update the Anchor store.
*/
import type { AnchorCbType, AnchorType } from '../../edges/types/types';
import { stores } from '../../store/models/store';
import { getAnchors } from '../controllers/util';
/** Class representing an Anchor that implements Anchortype.
* @param {string} id The id of the Anchor
* @param {string} nodeId The id of the Node which the instantiated Anchor will be attached to
* @param {string} edgeId The id of the Edge which connects to the instantiated Anchor
* @param {'source' | 'target'} sourceOrTarget Specify the Anchor is a source or target
* @param {number} positionX The 'X' coordinate of the Anchor
* @param {number} positionY The 'Y' coordinate of the Anchor
* @param {function} callback The callback function that will determine the position of this Anchor
* @param {string} canvasId The canvasId of the Svelvet component that will hold this Anchor
* @param { number } angle This is the orientation of the anchor and is used to make sure bezier/step curves are rendered perpendicular to the node. Angles are defined along the unit circle. EX: 0 = right side of node, 180 = left side of node.
*/
export class Anchor implements AnchorType {
constructor(
public id: string,
public nodeId: string,
public edgeId: string,
public sourceOrTarget: 'source' | 'target',
public positionX: number,
public positionY: number,
public callback: AnchorCbType,
public canvasId: string,
public angle: 0 | 90 | 180 | 270
) {}
getOtherAnchorId() {
const store = stores[this.canvasId];
const anchors = getAnchors(store, { edgeId: this.edgeId });
if (anchors.length !== 2)
throw 'something is wrong, every anchor should have exactly one other associated anchor';
const anchor = anchors.filter((anchor) => anchor.id !== this.id)[0];
return anchor.id;
}
/**
* setPositionFromNode will invoke the user-defined callback to calculate the position of the Anchor, set the position of the Anchor in the anchorsStore, and also update the Edge position accordingly.
*/
setPositionFromNode() {
// calculate the new position of the anchor using user-defined callback
this.callback();
const { edgesStore } = stores[this.canvasId];
// update edges
edgesStore.update((edges) => {
const edge = edges[this.edgeId];
if (this.sourceOrTarget === 'source') {
edge.sourceX = this.positionX;
edge.sourceY = this.positionY;
} else {
edge.targetX = this.positionX;
edge.targetY = this.positionY;
}
return { ...edges };
});
}
/**
* updateEdges will update the edgesStore based on Anchor's change.
*/
updateEdges() {
const { edgesStore } = stores[this.canvasId];
/** update edges by deconstructing edges store and setting its new vlue to stores[this.canvasID] */
edgesStore.update((edges) => {
const edge = edges[this.edgeId];
// this means that no edge was found, just return without doing anything
if (edge === undefined) {
return { ...edges };
}
if (this.sourceOrTarget === 'source') {
edge.sourceX = this.positionX;
edge.sourceY = this.positionY;
} else {
edge.targetX = this.positionX;
edge.targetY = this.positionY;
}
return { ...edges };
});
}
/**
* setPosition will update the positionX and positionY of the Anchor and also cascade changes to related Edge.
* @param x The X coordinate of the new position for the Anchor
* @param y The Y coordinate of the new position for the Anchor
*/
setPosition(x: number, y: number) {
this.positionX = x;
this.positionY = y;
this.updateEdges();
}
/**
* Anchor.setPositionFromMovement works similarly to Anchor.setPosition. But setPosition is more powerful and we recommend using setPosition whenever possible and in the future, setPositionFromMovement can be removed.
* @param movementX The mouse movement value on the X-axis
* @param movementY The mouse movement value on the Y-axis
*/
setPositionFromMovement(movementX: number, movementY: number) {
this.positionX += movementX;
this.positionY += movementY;
const { edgesStore } = stores[this.canvasId];
edgesStore.update((edges) => {
const edge = edges[this.edgeId];
if (this.sourceOrTarget === 'source') {
edge.sourceX += movementX;
edge.sourceY += movementY;
} else {
edge.targetX += movementX;
edge.targetY += movementY;
}
return { ...edges };
});
}
}
@@ -1,95 +0,0 @@
import { findStore } from '../../store/models/store'
import type { UserEdgeType } from '../../types/types'
import type { EdgeType } from '../../store/types/types'
import { stores } from '../../store/models/store'
import { getAnchors, getAnchorFromEdge } from '../../edges/controllers/util'
/**
* Class Edge that implements EdgeType.
* @param id The id of the Edge
* @param sourceX The X coordinate of the source Anchor
* @param sourceY The Y coordinate of the source Anchor
* @param targetX The X coordinate of the target Anchor
* @param targetY The Y coordinate of the target Anchor
* @param canvasId The canvasId of the Svelvet component that holds the instantiated Edge
* @param label The label of the Edge
* @param type The type of the Edge (options: 'straight', 'smoothstep', 'step', or 'bezier'). If user doesn't specify, the type will default to 'bezier'.
* @param labelBgColor The background color of the Edge label
* @param labelTextColor The text color of the Edge label
* @param edgeColor The color of the Edge
* @param animate Boolean value to specify whether the Edge should be animated
* @param noHandle Boolean value but looks like it is already depracated and can be removed without damage
* @param arraw Boolean value to specify whether the Edge displays an arrow near its target Anchor
*/
export class Edge implements EdgeType {
constructor(
public id: string,
public sourceX: number,
public sourceY: number,
public targetX: number,
public targetY: number,
public canvasId: string,
public label: string,
public type: 'straight' | 'smoothstep' | 'step' | 'bezier',
public labelBgColor: string,
public labelTextColor: string,
public edgeColor: string,
public animate: boolean,
public noHandle: boolean,
public arrow: boolean,
public clickCallback: Function,
public className: string,
public offset?: number
) {}
/**
* delete is going to delete the Edge and also delete associated Anchors
*/
delete() {
const store = stores[this.canvasId]
const { anchorsStore, edgesStore } = store
const sourceAnchor = getAnchorFromEdge(store, this.id, 'source') // this is a bit wasteful
const targetAnchor = getAnchorFromEdge(store, this.id, 'target')
anchorsStore.update((anchors) => {
for (const anchorId in anchors) {
if (anchorId === sourceAnchor.id || anchorId == targetAnchor.id) delete anchors[anchorId]
}
return { ...anchors }
})
edgesStore.update((edges) => {
delete edges[this.id]
return { ...edges }
})
}
/**
* setExportableData will construct an object that holds all the edge data that can be exported. This is needed for the Exporting Diagram feature.
* @returns The object of exportable edge data. The format of the object should be as close as what user initially passes in to Svelvet.
*/
setExportableData() {
const exportableData: UserEdgeType = {
id: this.id,
label: this.label,
type: this.type,
labelBgColor: this.labelBgColor,
labelTextColor: this.labelTextColor,
edgeColor: this.edgeColor,
animate: this.animate,
noHandle: this.noHandle,
arrow: this.arrow,
source: 'dummy', // these will be set later
target: 'dummy' // these will be set later
}
// set source, target on exportableData
const store = findStore(this.canvasId)
const anchors = getAnchors(store, { edgeId: this.id })
if (anchors.length !== 2) throw 'there should be two anchors per edge'
for (const anchor of anchors) {
if (anchor.sourceOrTarget === 'target') exportableData.target = anchor.nodeId
if (anchor.sourceOrTarget === 'source') exportableData.source = anchor.nodeId
}
return exportableData
}
}
@@ -1,19 +0,0 @@
export type AnchorCbType = {
(): void;
type: 'dynamic' | 'fixed';
};
export interface AnchorType {
id: string; // note that the user never specifies an anchor and they are generated dynamically. id will be a random string.
nodeId: string;
edgeId: string;
sourceOrTarget: 'source' | 'target';
positionX: number;
positionY: number;
callback: AnchorCbType; // callback is used to calculate positionX, positionY based on parent node's data, and set the anchor position // TODO: rename to something better
angle: number;
setPositionFromNode: Function;
setPosition: Function;
updateEdges: Function;
getOtherAnchorId: Function;
}
@@ -1,108 +0,0 @@
<script lang="ts">
import { findStore } from '../../../store/models/store'
import { getEdgeById } from '../../../edges/controllers/util'
import EdgeText from '../Edges/EdgeText.svelte'
import { get } from 'svelte/store'
import type { EdgeProps } from '../Edges/types'
export let baseEdgeProps: EdgeProps
export let canvasId
// destructuring the props passed in from the parent component
$: ({ path, animate, arrow, label, labelBgColor, labelTextColor, edgeColor, centerX, centerY } =
baseEdgeProps)
// setting edge text props
$: edgeTextProps = {
label: label,
labelBgColor: labelBgColor,
labelTextColor: labelTextColor,
centerX: centerX,
centerY: centerY
}
// Click event handlers
// At some point in the future, it would be good to refactor event handling to use the flux architecture
// ie, events will create an action that will be dispatched to some centralized reducer.
// or in other words, the creators of Redux knew what they were doing.
// The advantage of this re-design would be greater modularity; views would be agnostic to the exact features implmemented,
// and they would be only responsible to detecting events and dispatch actions.
const edgeId = baseEdgeProps.id
const store = findStore(canvasId)
const edge = getEdgeById(store, edgeId)
const highlightEdgesOption = get(store.highlightEdgesOption)
const handleRightClick = () => {
const store = findStore(canvasId)
const { editableOption } = store
// handles edgeEdit feature
if (get(editableOption)) store.edgeEditModal.set(edgeId)
}
const handleClick = () => {
const store = findStore(canvasId)
const edge = getEdgeById(store, edgeId)
// handles edge clickCallback feature
if (edge.clickCallback) edge.clickCallback(edge)
}
const defaultArrow = `0 0, 9 4.5, 0 9`
</script>
<defs>
<marker id="arrow" markerWidth="9" markerHeight="9" refX="8" refY="4" orient="auto">
<polygon points={defaultArrow} fill="gray" />
</marker>
</defs>
<!-- This is an invisible edge that is used to implement event events, because the visible edge is thin and hard to click on. It
highlights on hover -->
{#if highlightEdgesOption}
<path
id={`edgeSelector`}
d={path}
fill="transparent"
stroke={'red'}
stroke-opacity="0"
stroke-width="10"
on:contextmenu={handleRightClick}
on:click={handleClick}
on:keypress={() => {}}
/>
{/if}
{#if arrow}
<path
class={animate ? `animate ${edge.className}` : `${edge.className}`}
d={path}
fill="transparent"
stroke={edgeColor ? edgeColor : 'gray'}
marker-end="url(#arrow)"
aria-label="svg-path"
/>
{:else}
<path
class={animate ? `animate ${edge.className}` : `${edge.className}`}
d={path}
fill="transparent"
stroke={edgeColor ? edgeColor : 'gray'}
aria-label="svg-path"
/>
{/if}
{#if edgeTextProps.label}
<EdgeText {edgeTextProps} />
{/if}
<style>
.animate {
stroke-dasharray: 5;
animation: dash 50000s linear;
}
@keyframes dash {
from {
stroke-dashoffset: 1000000;
}
}
#edgeSelector:hover {
stroke: 'red';
stroke-opacity: 0.5;
}
</style>
@@ -1,48 +0,0 @@
<script lang="ts">
import type { EdgeTextProps } from './types'
// destructuring props to pass into BaseEdge component
export let edgeTextProps: EdgeTextProps
$: ({ label, labelBgColor, labelTextColor, centerX, centerY } = edgeTextProps)
const shiftRectY: number = 7
$: pxRatio = label.length < 3 ? 9 : 7
// determine the center point of the edge to be used in the EdgeText component
$: textCenterX = centerX
$: textCenterY = centerY
// determine width of rect to render based on label.length (removing spaces)
// pxRatio is an estimate of how many pixels 1 character might take up
// pxRatio not 100% accurate as font is not monospace
$: spaces = label.split(' ').length - 1
$: newLength = label.length - spaces
$: labelPx = newLength * pxRatio
</script>
{#if typeof label === 'undefined' || !label}
{null}
{:else}
<g>
<rect
class="EdgeTextBg"
data-testid="edge-text-bg"
fill={labelBgColor ? labelBgColor : 'white'}
x={textCenterX - labelPx / 2}
y={textCenterY - shiftRectY}
width={labelPx}
height={16}
/>
<text
class="EdgeText"
x={textCenterX}
y={textCenterY}
font-size="12px"
dominant-baseline="central"
text-anchor="middle"
fill={labelTextColor ? labelTextColor : 'black'}
>
{label}
</text>
</g>
{/if}
@@ -1,175 +0,0 @@
<script lang="ts">
import BaseEdge from './BaseEdge.svelte'
const Position = { Left: 'left', Right: 'right', Top: 'top', Bottom: 'bottom' }
import { findStore } from '../../../store/models/store'
import { getAnchorFromEdge } from '../../../edges/controllers/util'
function calculateControlOffset(distance, curvature) {
if (distance >= 0) {
return 0.9 * distance
} else {
return curvature * 25 * Math.sqrt(-distance)
}
}
// get the control point for the bezier curve (in the middle of the edge)
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }) {
let ctX, ctY
switch (pos) {
case Position.Left:
{
ctX = x1 - calculateControlOffset(x1 - x2, c)
ctY = y1
}
break
case Position.Right:
{
ctX = x1 + calculateControlOffset(x2 - x1, c)
ctY = y1
}
break
case Position.Top:
{
ctX = x1
ctY = y1 - calculateControlOffset(y1 - y2, c)
}
break
case Position.Bottom:
{
ctX = x1
ctY = y1 + calculateControlOffset(y2 - y1, c)
}
break
}
return [ctX, ctY]
}
// returns string to pass into edge 'path' svg d attribute (where to be drawn)
// referenced from ReactFlow.dev
function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25
}) {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature
})
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`
}
// returns string to pass into edge 'path' svg d attribute for the straight line
function getStraightLinePath({ sourceX, sourceY, targetX, targetY }) {
return `M${sourceX},${sourceY} L${sourceX},${targetY - 50}`
}
// determining center of the bezier curve to know where to place the bezier edge text label
function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25
}) {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature
})
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature
})
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX =
sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125
const centerY =
sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125
const xOffset = Math.abs(centerX - sourceX)
const yOffset = Math.abs(centerY - sourceY)
return [centerX, centerY, xOffset, yOffset]
}
export let canvasId: string
export let edgeId: string
const store = findStore(canvasId)
const { edgesStore } = store
let edge
$: edge = $edgesStore[edgeId]
let bezierParams, lineParams
$: {
const store = findStore(canvasId)
const sourceAnchor = getAnchorFromEdge(store, edge.id, 'source')
const targetAnchor = getAnchorFromEdge(store, edge.id, 'target')
const mapAngle = { 0: 'right', 90: 'top', 180: 'left', 270: 'bottom' }
if (edge.targetY - edge.sourceY > 100) {
bezierParams = {
sourceX: edge.sourceX + (edge.offset ?? 0),
sourceY: edge.targetY - 100,
sourcePosition: mapAngle[sourceAnchor.angle],
targetX: edge.targetX + (edge.offset ?? 0),
targetY: edge.targetY,
targetPosition: mapAngle[targetAnchor.angle],
curvature: 0.25
}
lineParams = {
sourceX: edge.sourceX + (edge.offset ?? 0),
sourceY: edge.sourceY,
targetX: edge.targetX + (edge.offset ?? 0),
targetY: edge.targetY - 50
}
} else {
bezierParams = {
sourceX: edge.sourceX + (edge.offset ?? 0),
sourceY: edge.sourceY,
sourcePosition: mapAngle[sourceAnchor.angle],
targetX: edge.targetX + (edge.offset ?? 0),
targetY: edge.targetY,
targetPosition: mapAngle[targetAnchor.angle],
curvature: 0.25
}
lineParams = {}
}
}
// pass in params to function that returns a string value for SVG path d attribute (where to be drawn)
$: bezierPath = getSimpleBezierPath(bezierParams)
$: linePath = lineParams ? getStraightLinePath(lineParams) : ''
$: [bezierCenterX, bezierCenterY] = getSimpleBezierCenter(bezierParams)
// pass necessary values to BaseEdge component
// BaseEdge renders a 'base' path that can be customized by parent Edge components
$: baseEdgeProps = {
...edge,
path: `${bezierPath} ${linePath}`,
centerX: bezierCenterX,
centerY: bezierCenterY
}
</script>
<BaseEdge {baseEdgeProps} {canvasId} />
@@ -1,157 +0,0 @@
<script>
import BaseEdge from './BaseEdge.svelte'
import { getCenter } from './utils'
const Position = {}
Position['Left'] = 'left'
Position['Right'] = 'right'
Position['Top'] = 'top'
Position['Bottom'] = 'bottom'
// These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes
// from bottom to the left and "leftBottomCorner" goes from left to the bottom.
// We have to consider the direction of the paths because of the animated lines.
const bottomLeftCorner = (x, y, size) => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}`
const leftBottomCorner = (x, y, size) => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}`
const bottomRightCorner = (x, y, size) => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}`
const rightBottomCorner = (x, y, size) => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}`
const leftTopCorner = (x, y, size) => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}`
const topLeftCorner = (x, y, size) => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}`
const topRightCorner = (x, y, size) => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`
const rightTopCorner = (x, y, size) => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`
// returns string to pass into edge 'path' svg d attribute (where to be drawn)
export function getSmoothStepPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
borderRadius = 5,
centerX,
centerY
}) {
const [_centerX, _centerY, offsetX, offsetY] = getCenter({
sourceX,
sourceY,
targetX,
targetY
})
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX))
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY))
const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY)
const leftAndRight = [Position.Left, Position.Right]
const cX = typeof centerX !== 'undefined' ? centerX : _centerX
const cY = typeof centerY !== 'undefined' ? centerY : _centerY
let firstCornerPath = null
let secondCornerPath = null
// for non-mixed edge top/bottom
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY
? bottomLeftCorner(sourceX, cY, cornerSize)
: topLeftCorner(sourceX, cY, cornerSize)
secondCornerPath =
sourceY <= targetY
? rightTopCorner(targetX, cY, cornerSize)
: rightBottomCorner(targetX, cY, cornerSize)
} else {
firstCornerPath =
sourceY < targetY
? bottomRightCorner(sourceX, cY, cornerSize)
: topRightCorner(sourceX, cY, cornerSize)
secondCornerPath =
sourceY < targetY
? leftTopCorner(targetX, cY, cornerSize)
: leftBottomCorner(targetX, cY, cornerSize)
}
// for non-mixed edge left/right
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY
? rightTopCorner(cX, sourceY, cornerSize)
: rightBottomCorner(cX, sourceY, cornerSize)
secondCornerPath =
sourceY <= targetY
? bottomLeftCorner(cX, targetY, cornerSize)
: topLeftCorner(cX, targetY, cornerSize)
} else if (
(sourcePosition === Position.Right && targetPosition === Position.Left) ||
(sourcePosition === Position.Left && targetPosition === Position.Right) ||
(sourcePosition === Position.Left && targetPosition === Position.Left)
) {
// and sourceX > targetX
firstCornerPath =
sourceY <= targetY
? leftTopCorner(cX, sourceY, cornerSize)
: leftBottomCorner(cX, sourceY, cornerSize)
secondCornerPath =
sourceY <= targetY
? bottomRightCorner(cX, targetY, cornerSize)
: topRightCorner(cX, targetY, cornerSize)
}
// for mixed edges (top/bottom to left/right) OR (left/right to top/bottom)
} else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY
? rightTopCorner(targetX, sourceY, cornerSize)
: rightBottomCorner(targetX, sourceY, cornerSize)
} else {
firstCornerPath =
sourceY <= targetY
? leftTopCorner(targetX, sourceY, cornerSize)
: leftBottomCorner(targetX, sourceY, cornerSize)
}
secondCornerPath = ''
} else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (sourceX <= targetX) {
firstCornerPath =
sourceY <= targetY
? bottomLeftCorner(sourceX, targetY, cornerSize)
: topLeftCorner(sourceX, targetY, cornerSize)
} else {
firstCornerPath =
sourceY <= targetY
? bottomRightCorner(sourceX, targetY, cornerSize)
: topRightCorner(sourceX, targetY, cornerSize)
}
secondCornerPath = ''
}
return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}`
}
export let edge
export let borderRadius = 5
export let canvasId
import { findStore } from '../../../store/models/store'
import { getAnchorFromEdge } from '../../../edges/controllers/util'
let params
$: {
const store = findStore(canvasId)
const sourceAnchor = getAnchorFromEdge(store, edge.id, 'source')
const targetAnchor = getAnchorFromEdge(store, edge.id, 'target')
const mapAngle = { 0: 'right', 90: 'top', 180: 'left', 270: 'bottom' }
params = {
sourceX: edge.sourceX,
sourceY: edge.sourceY,
targetX: edge.targetX,
targetY: edge.targetY,
sourcePosition: mapAngle[sourceAnchor.angle],
targetPosition: mapAngle[targetAnchor.angle],
borderRadius: borderRadius
}
}
$: [centerX, centerY] = getCenter(params)
$: path = getSmoothStepPath(params)
$: baseEdgeProps = {
...edge,
path: path,
centerX: centerX,
centerY: centerY
}
</script>
<BaseEdge {baseEdgeProps} {canvasId} />
@@ -1,8 +0,0 @@
<script>
import SmoothStepEdge from './SmoothStepEdge.svelte';
export let edge;
export let canvasId;
</script>
<!-- Utilizes the SmoothStepEdge Component and sets borderRadius to zero to give a sharp edge -->
<SmoothStepEdge {edge} borderRadius={0} {canvasId} />
@@ -1,58 +0,0 @@
import type { XYPosition, Position } from './utils';
export interface Node<T = any> {
id: number;
position: XYPosition;
data: T;
width: number;
height: number;
bgColor?: string;
fontSize?: number;
borderColor?: string;
borderRadius?: number;
textColor?: string;
clickCallback?: Function;
image?: boolean;
src?: string;
sourcePosition?: 'left' | 'right' | 'top' | 'bottom';
targetPosition?: 'left' | 'right' | 'top' | 'bottom';
}
export interface Edge {
id: string;
source: number;
target: number;
label?: string;
labelBgColor?: string;
labelTextColor?: string;
edgeColor?: string;
type?: string;
animate?: boolean;
noHandle?: boolean;
arrow?: boolean;
}
export interface DerivedEdge extends Edge {
sourceX: number;
sourceY: number;
sourcePosition: Position;
targetX: number;
targetY: number;
targetPosition: Position;
}
export interface EdgeProps extends DerivedEdge {
path: string;
centerX: number;
centerY: number;
}
export interface EdgeTextProps {
label?: any;
labelBgColor?: string;
labelTextColor?: string;
centerX: number;
centerY: number;
}
export type HandleType = 'source' | 'target';
@@ -1,84 +0,0 @@
// enumerable values (static) set for Position
export enum Position {
Left = 'left',
Right = 'right',
Top = 'top',
Bottom = 'bottom',
}
// interface for XYPosition to use in nodes and edges
export interface XYPosition {
x: number;
y: number;
}
// type for z axis positioning with D3
export type XYZPosition = XYPosition & { z: number };
// interface for changing dimensions of Viewport
export interface Dimensions {
width: number;
height: number;
}
// interface of Rect divs in zoompane
export interface Rect extends Dimensions, XYPosition {}
// interface of Box using XYPosition of nodes
export interface Box extends XYPosition {
x2: number;
y2: number;
}
// D3 type array for Transform
export type Transform = [number, number, number];
//
// export type CoordinateExtent = [[number, number], [number, number]];
export interface GetCenterParams {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition?: Position;
targetPosition?: Position;
}
//needed for getCenter funciotn
const LeftOrRight = [Position.Left, Position.Right];
//used to determine the position for edge text on a Smooth or Step Edge
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition);
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition);
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge =
(sourceIsLeftOrRight && !targetIsLeftOrRight) ||
(targetIsLeftOrRight && !sourceIsLeftOrRight);
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0;
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset;
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY);
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset;
return [centerX, centerY, xOffset, yOffset];
}
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
return [centerX, centerY, xOffset, yOffset];
};
@@ -1,16 +0,0 @@
import type { StoreType } from '../../store/types/types';
import { get } from 'svelte/store';
/**
* getNodeById will look for the targeted Node that has the same id provided in the Svelvet component store.
*
* @param store The Svelvet store containing the state of a Svelvet component
* @param id The id of the targeted Node
* @returns The targeted Node object in store.nodesStore
*/
export function getNodeById(store: StoreType, id: string) {
const nodesStore = get(store.nodesStore);
const node = nodesStore[id];
return node;
}
@@ -1,156 +0,0 @@
/** this is where we create our node store */
import type { NodeType } from '../../store/types/types'
import { get } from 'svelte/store'
import { stores } from '../../store/models/store'
/** A Node class that implements NodeType interface
* @param {string} id The id of the Node
* @param {number} positionX The X-axis position of the Node (left top corner of the Node)
* @param {number} positionY The Y-axis position of the Node (left top corner of the Node)
* @param {number} width The width of the Node
* @param {number} height The height of the Node
* @param {string} bgColor The background color of the node
* @param {object} data A data object that user can specify; possible keys are 'label' and 'custom';
* @param {string} canvasId The canvasId of the Svelvet component that the instantiated Node will be on.
* @param {string} borderColor The border color of the Node
* @param {boolean} image A boolean set to true if the Node needs to display an image
* @param {string} src The src link for the image; image and src are closely tied and a src link is only needed when image sets to true
* @param {string} textColor The color of the text in the Node
* @param {string} borderRadius The border radius of the Node
* @param {string} childNodes An array of node ids that will be grouped as child nodes of this Node. This is for the GroupNodes feature. The current implementation of this feature works one way but not the other (when you drag the parent node, the child nodes will move as a group but when you drag the child node, the parent node would not move along)
* @param {string} className The custom class name if user specifies. This is for the custom className feature for Node.
*/
export class Node implements NodeType {
constructor(
public id: string,
public positionX: number,
public positionY: number,
public width: number,
public height: number,
public bgColor: string,
public data: object,
public canvasId: string,
public borderColor: string,
public image: boolean,
public src: string,
public textColor: string,
public borderRadius: number,
public childNodes: string[],
public className: string,
public clickCallback: Function
) {}
/**
* setPositionFromMovement will update the positionX and positionY of the Node when user drags a Node around on the canvas, reflect the changes in real time in the nodesStore, and also cascade the changes to all relative elements like Anchors and Edges.
* @param {number} movementX The mouse movement value on the X-axis
* @param {number} movementY The mouse movement value on the Y-axis
*/
setPositionFromMovement(movementX: number, movementY: number) {
const { nodesStore, anchorsStore, potentialAnchorsStore, boundary, lockedOption } =
stores[this.canvasId]
if (get(lockedOption)) return // don't do anything if locked is enabled
// boundary sets the boundary of the canvas, or else it is false if their is no boundary
// check if out of bounds, and if so return without doing anything
const boundaryObj = get(boundary)
if (
typeof boundaryObj === 'object' &&
(this.positionX + this.width + movementX >= boundaryObj.x ||
this.positionY + this.height + movementY >= boundaryObj.y ||
this.positionY + movementY <= 0 ||
this.positionX + movementX <= 0)
)
return
//update all necessary data
this.positionX += movementX
this.positionY += movementY
// update children
nodesStore.update((nodes) => {
if (this.childNodes)
for (const childNodeId of this.childNodes)
nodes[childNodeId].setPositionFromMovement(movementX, movementY)
return { ...nodes }
})
//update all the anchors on the node in the anchorsStore
anchorsStore.update((anchors) => {
for (const anchorId in anchors) {
if (anchors[anchorId].nodeId === this.id) {
anchors[anchorId].setPositionFromNode()
}
}
return { ...anchors }
})
//update all the anchors on the node in the anchorsStore
potentialAnchorsStore.update((anchors) => {
for (const anchorId in anchors) {
if (anchors[anchorId].nodeId === this.id) {
anchors[anchorId].callback() // we don't have to worry about setting partner anchors/etc;
}
}
return { ...anchors }
})
}
/**
* setSizeFromMovement will update the width and height of the Node when user resizes the Node by dragging at the right bottom corner (where the ResizedNode attached), reflect the changes in real time in the nodesStore, and also cascade the changes to all relative elements like Anchors and potential Anchors.
*
* @param movementX The mouse movement value on the X-axis
* @param movementY The mouse movement value on the Y-axis
*/
setSizeFromMovement(movementX: number, movementY: number) {
this.width += movementX
this.height += movementY
const { anchorsStore, potentialAnchorsStore } = stores[this.canvasId]
//Updates the anchor so it follows the node's position as the dimensions change
anchorsStore.update((anchors) => {
for (const anchorId in anchors) {
if (anchors[anchorId].nodeId === this.id) {
anchors[anchorId].setPositionFromNode()
//anchors[anchorId].setPosition(movementX, movementY);
}
}
return { ...anchors }
})
//update all the anchors on the node in the anchorsStore
potentialAnchorsStore.update((anchors) => {
for (const anchorId in anchors) {
if (anchors[anchorId].nodeId === this.id) {
anchors[anchorId].callback() // we don't have to worry about setting partner anchors/etc;
}
}
return { ...anchors }
})
}
/**
* setExportableData is going to construct an object that holds all the node data that can be exported. This method is used for Exporting Diagrams feature.
*
* @returns An object with all the exportable data of the Node. The format of this object should align with the original format of node data user provided.
*/
setExportableData() {
const exportableData = {
id: this.id,
// canvasId: this.canvasId,
width: this.width,
height: this.height,
position: { x: this.positionX, y: this.positionY },
data: this.data,
bgColor: this.bgColor,
borderColor: this.borderColor,
textColor: this.textColor,
borderRadius: this.borderRadius,
image: this.image,
src: this.src,
childNodes: this.childNodes,
customClassName: this.className
}
return exportableData
}
}
@@ -1,91 +0,0 @@
<script lang="ts">
import { afterUpdate } from 'svelte'
import { findStore } from '../../store/models/store'
import type { NodeType } from '../../store/types/types'
import { forceCssHeightAndWidth } from '../../customCss/controllers/getCss'
export let node: NodeType
export let canvasId: string
export let isCustom = false
const store = findStore(canvasId)
const { lockedOption } = store
// forceCssHeightAndWidth forces the size of the node to be defined by CSS
afterUpdate(() => {
if (node.className) forceCssHeightAndWidth(store, node)
})
const mousedown = (e) => {
if (node.clickCallback) node.clickCallback(node)
}
</script>
<!-- on:wheel prevents page scroll when using mousewheel in the Node -->
<div
on:mousedown={mousedown}
on:touchstart={mousedown}
class="{isCustom ? 'CustomNode' : 'Node'} {node.className}"
style="left: {node.positionX}px;
top: {node.positionY}px;
width: {node.width}px;
height: {node.height}px;
{isCustom
? ''
: `
background-color: ${node.bgColor ?? 'white'};
border-color: ${node.borderColor};
border-radius: ${node.borderRadius}px;
color: ${node.textColor};
cursor: ${$lockedOption ? 'default' : 'grab'};`}"
id="svelvet-{node.id}"
>
<!-- This executes if node.image is present without node.label -->
{#if node.image}
<img
src={node.src}
alt=""
style="width: {node.width * 0.75}px;
height: {node.height * 0.75}px;
overflow: hidden;"
/>
{/if}
<slot />
</div>
<style>
.CustomNode {
position: absolute;
display: grid;
justify-content: center;
align-items: center;
font-size: 14px;
text-align: center;
pointer-events: auto;
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.2);
}
.Node {
position: absolute;
display: grid;
user-select: none;
justify-content: center;
overscroll-behavior: auto;
align-items: center;
font-size: 14px;
text-align: center;
border: solid 1px black;
border-radius: 5px;
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.2);
pointer-events: auto; /* this is needed for pointer events to work since we disable them in graphview */
}
/* the default behavior when click/dragging an image is to move it. This interferes with node dragging. We disable pointer events on img to prevent this */
/* Alternatively, we could use e.preventDefault() on mouseDown. However, this interferes with embedded Svelvet forms */
img {
pointer-events: none;
}
</style>
@@ -1,103 +0,0 @@
/*
Important functions (in order of how likely I think you are to use them):
findStore(canvasId: string)
- Input: canvasId. There can be multiple Svelvet canvases on the same page, and each has their own store
- Returns: store
- Notes: You will need to call this function on every component you make
getNodes(store, filter)
- Description: this function returns of list of Node objects
- Input: store: the store where you get the list of Node objects from
- filter: an object where you can filter the array. For example, if you want to find nodes with an id of 'sdf-2d3fs' and positionX=35, then you
would set filter = {id: 'sdf-2d3fs, positionX: 35}.
Right now, you can only test for equality.
- Notes: This is a very mongoose way of retrieving information. Given the relational nature of our stores, maybe it would be better to use an SQL-like syntax
This array is non-reactive (ie, you cannot use information from this array to force a re-render of a Svelte component)
createStoreEmpty(canvasId: string)
- Input: canvasId.
- Returns: store
- Notes. This should be called once every time you initialize a new Svelvet canvas, (ie, only in the Svelvet.svelte file)
populateSvelvetStoreFromUserInput(canvasId, nodes, edges)
- canvasId: this the the canvasId of the Svelvet component you are creating a store for
- nodes: this is an array of objects containing node info that is defined by the user. NOTE THAT THE STRUCTURE DIFFERS FROM THE NODES CLASS
The whole point of populateSvelvetStoreFromUserInput is to convert nodes into proper Svelvet Node objects. An example of nodes is in
$routes/testingplayground/index.svelte
- edges: same as nodes, this is an array of objects containing edge info THAT IS DIFFERENT FROM THE EDGE CLASS.
- Returns: store
*/
import { findStore, stores } from '../models/store'
import { writable } from 'svelte/store'
import type { StoreType } from '../types/types'
import type { UserNodeType, UserEdgeType } from '../../types/types'
import { populateAnchorsStore, populateNodesStore, populateEdgesStore } from './util'
/**
* createStoreEmpty will initialize a new Svelvet store with a unique canvasId.
* If you have multiple Svelvet components on the page, the stores object will look like the following example:
* const stores = \{
* canvasId-1: store of Svelvet component 1,
* canvasId-2: store of Svelvet component 2,
* canvasId-3: store of Svelvet component 3,
* \}
* Notes: This should be called once every time you initialize a new Svelvet canvas, (ie, only in the Svelvet.svelte file).
* This function will initialize an empty store for the Svelvet component and should be followed by invoking populateSvelvetStoreFromUserInput to populate all the initial state from the user input.
*
* @param canvasId The canvasId of the newly created Svelvet component
* @returns An empty store for the newly created Svelvet component.
*/
export function createStoreEmpty(canvasId: string): StoreType {
stores[canvasId] = {
nodesStore: writable({}),
edgesStore: writable({}),
anchorsStore: writable({}),
potentialAnchorsStore: writable({}),
widthStore: writable(600),
heightStore: writable(600),
backgroundStore: writable(false),
movementStore: writable(true),
nodeSelected: writable(false),
nodeIdSelected: writable(-1),
d3Scale: writable(1),
options: writable({}),
temporaryEdgeStore: writable([]),
nodeCreate: writable(false), // this option sets whether the "nodeEdit" feature is enabled
boundary: writable(false),
edgeEditModal: writable(null), // this is used for edgeEditModal feature. When an edge is right clicked, store.edgeEditModal is set to the edgeId string. This causes a modal to be rendered
lockedOption: writable(false),
editableOption: writable(false), // true if you want nodes/edges to be editable. See feature editEdges
d3ZoomParameters: writable({}), // this stores d3 parameters x, y, and zoom. This isn't used for anything other than giving users a way to access d3 zoom parameters if they want to build on top of Svelvet
highlightEdgesOption: writable(true) // option to turn on/off highlightable edges
}
return stores[canvasId]
}
/**
* populateSvelvetStoreFromUserInput will populate all the states and set these states into the Svelvet store initialized by invoking createStoreEmpty
*
* @param canvasId The canvasId of the Svelvet component you are creating a store for
* @param nodes This is an array of objects containing node info that is defined by the user. NOTE THAT THE STRUCTURE DIFFERS FROM THE NODES CLASS. The whole point of populateSvelvetStoreFromUserInput is to convert nodes into proper Svelvet Node objects. An example of nodes is in $routes/testingplayground/index.svelte
* @param edges Same as nodes, this is an array of objects containing edge info THAT IS DIFFERENT FROM THE EDGE CLASS.
*/
export function populateSvelvetStoreFromUserInput(
canvasId: string,
nodes: UserNodeType[],
edges: UserEdgeType[]
): void {
// find the store
const store = findStore(canvasId)
// populate store.nodesStore with user nodes
populateNodesStore(store, nodes, canvasId)
// populate store.anchorsStore with anchors. Note the userdoes not explictly define anchors; anchors are calculated from the edges
populateAnchorsStore(store, nodes, edges, canvasId)
// populate edges
populateEdgesStore(store, edges, canvasId)
}
@@ -1,18 +0,0 @@
import { findStore } from '../models/store';
import { get } from 'svelte/store';
export function getD3PositionX(canvasId) {
const store = findStore(canvasId);
const width = get(store.widthStore);
const x = width / 2 - get(store.d3ZoomParameters).x; // user input is shifted so that x=0, y=0 occurs in the center
return x;
}
export function getD3PositionY(canvasId) {
const store = findStore(canvasId);
const height = get(store.heightStore);
const y = height / 2 - get(store.d3ZoomParameters).y; // user input is shifted so that x=0, y=0 occurs in the center
return y;
}
export function getD3Zoom(canvasId) {
const store = findStore(canvasId);
return get(store.d3ZoomParameters).k;
}
@@ -1,215 +0,0 @@
const pkStringGenerator = () => (Math.random() + 1).toString(36).substring(7) // these are callbacks used to calculate anchor position relative to node
import { dynamicCbCreator, fixedCbCreator } from '../../edges/controllers/anchorCbDev'
import type { AnchorCbType, AnchorType } from '../../edges/types/types'
import type { UserEdgeType, UserNodeType } from '../../types/types'
import type { EdgeType, NodeType, StoreType } from '../types/types'
import { getAnchors } from '../../edges/controllers/util'
import { Anchor } from '../../edges/models/Anchor'
import { Edge } from '../../edges/models/Edge'
import { Node } from '../../nodes/models/Node'
/**
* Creates an Anchor on the targeted Node with infomation the userNode holds
* @param store An object containing the state of the Svelvet component. You can access the following through `store`: nodesStore, edgesStore, anchorsStore, etc.
* @param userNode A node that the user specifies. This is NOT the same as a Node object.
* @param sourceOrTarget User specified information of source or target
* @param canvasId The id of the canvas that holds the Anchor and its attached Node
* @param edge An edge that the user specifies. It should target the userNode as source or target. This is NOT the same as an Edge object
* @returns An Anchor object
*/
function createAnchor(
store: StoreType,
userNode: UserNodeType | null,
sourceOrTarget: 'source' | 'target',
canvasId: string,
edge: UserEdgeType
) {
// edge case
if (userNode === null) throw `you cannot create an anchor without a user node (for now)`
const edgeId = edge.id
const anchorId = pkStringGenerator()
// userCb is the appropriate source or taret callback from userEdge object. It is
// possible the user to NOT set userCb in which case userCb will be undefined
let userCb: Function | undefined
if (sourceOrTarget === 'target') userCb = edge.targetAnchorCb
else userCb = edge.sourceAnchorCb
// create anchor callbacks
let cb: AnchorCbType
if (userCb === undefined) cb = dynamicCbCreator(store, edgeId, anchorId)
else cb = fixedCbCreator(store, edgeId, anchorId, userNode.id, userCb)
// Create a new anchor.
const anchor = new Anchor(
anchorId,
userNode.id,
edgeId,
sourceOrTarget,
-1, // dummy variables for x,y,angle for now
-1, // dummy variables for x,y,angle for now
cb,
canvasId,
0 // dummy variables for x,y,angle for now
)
// return
return anchor
}
/**
* Populates edgesStore of Edges. This function does not return the edgesStore. Instead it sets the nodesStore of Svelvet store.
* @param store An object containing the state of the Svelvet component. You can access the following through `store`: nodesStore, edgesStore, anchorsStore, etc.
* @param edges An edge that the user specifies. This is NOT the same as a Edge object.
* @param canvasId The canvasId of the Svelvet component that holds the Edges
*/
export function populateEdgesStore(store: StoreType, edges: UserEdgeType[], canvasId: string) {
const edgesStore: { [key: string]: EdgeType } = {}
for (let i = 0; i < edges.length; i++) {
const userEdge = edges[i]
// { id: 'e1-2', source: 1, type: 'straight', target: 2, label: 'e1-2' },
// source is node.id for the source node
// target is node.id for the target node
// We need to get the anchors
const { id: edgeId } = userEdge
const anchors = getAnchors(store, { edgeId: edgeId })
// check that we have two anchors for every edge
if (anchors.length !== 2) throw 'We should have two anchors for every node'
// check that we have 1 source anchor and 1 target anchor. Since sourceOrTarget is typed to be either 'source'
// or 'target', it suffices to check whether there are two unique elements
if (new Set(anchors.map((e) => e.sourceOrTarget)).size !== 2)
throw 'we should have one source and one target anchor'
// get source and target anchor
let sourceAnchor, targetAnchor
if (anchors[0].sourceOrTarget === 'source') {
sourceAnchor = anchors[0]
targetAnchor = anchors[1]
} else {
sourceAnchor = anchors[1]
targetAnchor = anchors[0]
}
edgesStore[edgeId] = new Edge(
edgeId,
sourceAnchor.positionX,
sourceAnchor.positionY,
targetAnchor.positionX,
targetAnchor.positionY,
canvasId,
userEdge.label === undefined ? '' : userEdge.label,
userEdge.type === undefined ? 'bezier' : userEdge.type,
userEdge.labelBgColor === undefined ? 'white' : userEdge.labelBgColor,
userEdge.labelTextColor === undefined ? 'black' : userEdge.labelTextColor,
userEdge.edgeColor === undefined ? 'black' : userEdge.edgeColor,
userEdge.animate === undefined ? false : userEdge.animate,
userEdge.noHandle === undefined ? false : userEdge.noHandle,
userEdge.arrow === undefined ? false : userEdge.arrow,
userEdge.clickCallback === undefined ? () => {} : userEdge.clickCallback,
userEdge.className === undefined ? '' : userEdge.className,
userEdge.offset
)
}
store.edgesStore.set(edgesStore)
}
/**
* Finds userNode (with UserNodeType; Not the same as the Node) by the node id from nodesStore
* @param id The id of the Node in its nodesStore
* @param userNodes The array of userNodes (NOT the same as Node object)
* @returns The node that user specified or null if not found
*/
function findUserNodeById(id: string, userNodes: UserNodeType[]): UserNodeType | null {
for (let i = 0; i < userNodes.length; i++) {
const userNode = userNodes[i]
if (userNode.id === id) return userNode
}
return null
}
/**
* Populates the anchorsStore. This will overwrite any data in the AnchorsStore.
* @param store The Svelvet store containing the state of the Svelvet component
* @param nodes An array of user specified nodes
* @param edges An array of user specified edges
* @param canvasId The canvasId of the Svelvet component that holds the nodes and edges
*/
export function populateAnchorsStore(
store: StoreType,
nodes: UserNodeType[],
edges: UserEdgeType[],
canvasId: string
) {
// anchorsStore will populated and eventaully synchronized to store.anchorsStore
const anchorsStore: { [key: string]: AnchorType } = {}
// iterate through user edges. Note the user never explicitly defines anchors; we calculate anchors
// from the user edge/node information
for (let i = 0; i < edges.length; i++) {
const userEdge = edges[i]
// find the source and target userNodes. These will be used to create the nodeId foreign key and
// determine placement of the anchor based on userNode.targetPosition, useNode.sourcePosition
const { source: sourceNodeId, target: targetNodeId } = userEdge
const sourceUserNode = findUserNodeById(sourceNodeId, nodes)
const targetUserNode = findUserNodeById(targetNodeId, nodes)
// create source anchor
const sourceAnchor = createAnchor(store, sourceUserNode, 'source', canvasId, userEdge)
// create target anchor
const targetAnchor = createAnchor(store, targetUserNode, 'target', canvasId, userEdge)
// store source and target anchors
anchorsStore[sourceAnchor.id] = sourceAnchor
anchorsStore[targetAnchor.id] = targetAnchor
}
//populates the anchorsStore
store.anchorsStore.set(anchorsStore)
// set anchor positions. We can only set anchor positions after anchorsStore and nodesStore
// has been populated. TODO: maybe add a check to see that anchorsStore and NodesStore populated?
const anchors = getAnchors(store)
for (const anchor of anchors) anchor.callback()
}
/**
* Populates the nodesStore. This will overwrite any data in the nodesStore.
* @param store The Svelvet store containing the state of the Svelvet component
* @param nodes An array of user specifed nodes
* @param canvasId The canvasId of the Svelvet component that holds the nodes
*/
export function populateNodesStore(store: StoreType, nodes: UserNodeType[], canvasId: string) {
// this is the nodesStore object. THIS IS NOT THE SAME AS A NODESTORE
const nodesStore: { [key: string]: NodeType } = {}
// iterate through user nodes and create node objects
for (let i = 0; i < nodes.length; i++) {
const userNode: UserNodeType = nodes[i]
const nodeId = userNode.id
// TODO: move sanitizing default values to middleware
const node = new Node(
nodeId.toString(),
userNode.position.x,
userNode.position.y,
userNode.width,
userNode.height,
userNode.bgColor ?? 'white',
userNode.data,
canvasId,
userNode.borderColor === undefined ? 'black' : userNode.borderColor,
userNode.image === undefined ? false : userNode.image,
userNode.src === undefined ? '' : userNode.src,
userNode.textColor === undefined ? '' : userNode.textColor,
userNode.borderRadius === undefined ? 0 : userNode.borderRadius,
userNode.childNodes === undefined ? [] : userNode.childNodes,
userNode.className === undefined ? '' : userNode.className,
userNode.clickCallback === undefined ? () => {} : userNode.clickCallback
)
nodesStore[nodeId] = node
}
// This is actually what sets the store
store.nodesStore.set(nodesStore)
}
@@ -1,15 +0,0 @@
import type { StoreType } from '../types/types';
export function findStore(canvasId: string): StoreType {
return stores[canvasId]
}
/**
`store` is a dictionary of Svelvet stores.
* The reason why we have multiple Svelvet stores is to handle multiple canvases on the same page.
* A Svelvet store is the single source of truth for a canvas state.
* We discourage developers from interacting with stores directly; instead use the api methods in
`src/lib/controllers/storeApi.ts`. However, if need to direct access you can do so by importing:
`import { store } from 'src/lib/models/store';`
*/
export const stores: { [key: string]: StoreType } = {};
@@ -1,122 +0,0 @@
import type { Writable } from 'svelte/store'
import type { AnchorType } from '../../edges/types/types'
export interface ResizeNodeType {
id: string
nodeId: string
edgeId?: string
canvasId: string
anchorId?: string
positionX: number
positionY: number
setPositionAndCascade: Function
setPosition: Function
delete: Function
}
/*
Type for a single svelvet store
*/
export interface StoreType {
nodesStore: Writable<{ [key: string]: NodeType }>
edgesStore: Writable<{ [key: string]: EdgeType }>
anchorsStore: Writable<{ [key: string]: AnchorType }>
potentialAnchorsStore: Writable<{ [key: string]: PotentialAnchorType }>
widthStore: Writable<number>
heightStore: Writable<number>
backgroundStore: Writable<boolean>
movementStore: Writable<boolean>
nodeIdSelected: Writable<number>
nodeSelected: Writable<boolean> // this is used to stop d3 panning when node is being dragged
d3Scale: Writable<number> // for zoom and pan
options: Writable<{ [key: string]: any }>
temporaryEdgeStore: Writable<TemporaryEdgeType[]>
nodeCreate: Writable<boolean> // this option sets whether the "nodeEdit" feature is enabled
boundary: Writable<boolean | PositionType>
edgeEditModal: Writable<null | string> // this options is used to place the edgeEdit modal when an edge is right-clicked. null is no modal, positionType if modal should be placed at position defined by postionType.x, positionType.y
lockedOption: Writable<boolean>
editableOption: Writable<boolean>
d3ZoomParameters: Writable<{
[key: string]: number
}>
highlightEdgesOption: Writable<boolean>
}
export interface PositionType {
x: number
y: number
}
export interface NodeType {
id: string
width: number
height: number
positionX: number
positionY: number
bgColor: string
data: {
html?: any
custom?: { component: any; props?: any; cb?: (e: string, detail: any) => void }
img?: any
label?: string
}
canvasId: string
setPositionFromMovement: Function
setSizeFromMovement: Function
setExportableData: Function
borderColor: string
image: boolean
src: string
textColor: string
borderRadius: number
childNodes: string[]
className: string //This is for custom className for node
clickCallback: Function // user-supplied callback that executes when the node is clicked
}
export interface EdgeType {
id: string
sourceX: number
sourceY: number
targetX: number
targetY: number
canvasId: string
label: string
type: 'straight' | 'smoothstep' | 'step' | 'bezier'
labelBgColor: string
labelTextColor: string
edgeColor: string
animate: boolean
noHandle: boolean
arrow: boolean
clickCallback: Function
className: string
delete: Function
setExportableData: Function
}
export interface PotentialAnchorType {
id: string
nodeId: string
callback: Function // callback is used to calculate positionX, positionY based on parent node's data, and set the anchor position // TODO: rename to something better
positionX: number
positionY: number
angle: number
canvasId: string
delete: Function
}
export interface TemporaryEdgeType {
id: string
sourcePotentialAnchorId: string // this will always be set
sourceX: number
sourceY: number
targetPotentialAnchorId: string | null // this will be null until the temporary edge reaches another temporary anchor
targetX: number
targetY: number
canvasId: string
type: string
edgeColor: string
createEdge: Function
createNode: Function
}
@@ -1,3 +0,0 @@
# Types
This folder contains types exported to the user, as decribed in feature: `https://www.svelvet.io/docs/typescript/`
@@ -1,3 +0,0 @@
// this file exports user types
import type { UserEdgeType, UserNodeType } from './types'
export type { UserEdgeType, UserNodeType }
@@ -1,60 +0,0 @@
export interface UserNodeType {
id: string
width: number
height: number
bgColor?: string
data: {
html?: any
custom?: { component: any; props?: any; cb?: (e: string, detail: any) => void }
img?: any
}
position: { x: number; y: number }
borderColor?: string | undefined
image?: boolean
src?: string
textColor?: string
targetPosition?: 'left' | 'right' | 'top' | 'bottom'
sourcePosition?: 'left' | 'right' | 'top' | 'bottom'
borderRadius?: number
childNodes?: string[]
className?: string
clickCallback?: Function
}
export interface UserEdgeType {
id: string
source: string
target: string
sourceAnchorCb?: Function
targetAnchorCb?: Function
label?: string
labelBgColor?: string
labelTextColor?: string
edgeColor?: string
type?: 'straight' | 'smoothstep' | 'step' | 'bezier' | undefined
animate?: boolean
noHandle?: boolean
arrow?: boolean
clickCallback?: Function
className?: string
offset?: number
}
import { findStore } from '../store/models/store'
import { get } from 'svelte/store'
export function getD3PositionX(canvasId: string) {
const store = findStore(canvasId)
const width = get(store.widthStore)
const x = width / 2 - get(store.d3ZoomParameters).x // user input is shifted so that x=0, y=0 occurs in the center
return x
}
export function getD3PositionY(canvasId: string) {
const store = findStore(canvasId)
const height = get(store.heightStore)
const y = height / 2 - get(store.d3ZoomParameters).y // user input is shifted so that x=0, y=0 occurs in the center
return y
}
export function getD3Zoom(canvasId: string) {
const store = findStore(canvasId)
return get(store.d3ZoomParameters).k
}
+3 -9
View File
@@ -9,16 +9,10 @@ export const NODE = {
}
}
export function* createIdGenerator(): Generator<number, number, unknown> {
let id = 0
while (true) {
yield id++
}
}
export function getStateColor(
state: FlowStatusModule['type'] | undefined,
isDark: boolean
isDark: boolean,
lightModeBackground: string = '#dfe6ee'
): string {
switch (state) {
case 'Success':
@@ -32,6 +26,6 @@ export function getStateColor(
case 'WaitingForExecutor':
return isDark ? '#ea580c' : 'rgb(255, 208, 193)'
default:
return isDark ? '#2e3440' : '#fff'
return isDark ? '#2e3440' : lightModeBackground
}
}
@@ -9,7 +9,6 @@
import JobArgs from '$lib/components/JobArgs.svelte'
import { onDestroy, onMount } from 'svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import FlowGraph from '$lib/components/graph/FlowGraph.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { LogIn, AlertTriangle } from 'lucide-svelte'
@@ -20,6 +19,7 @@
import { setLicense } from '$lib/enterpriseUtils'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
$workspaceStore = $page.params.workspace
let rd = $page.url.href.replace($page.url.origin, '')
@@ -286,7 +286,7 @@
{#if job && job.raw_flow && !completed}
<h2 class="mt-10">Flow details</h2>
<div class="border border-gray-700">
<FlowGraph
<FlowGraphV2
modules={job.raw_flow?.modules}
failureModule={job.raw_flow?.failure_module}
notSelectable
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import FlowGraph from '$lib/components/graph/FlowGraph.svelte'
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
import { decodeState } from '$lib/utils'
let content = localStorage.getItem('svelvet')
@@ -8,7 +8,7 @@
: { modules: [], failureModule: undefined }
</script>
<FlowGraph fullSize {modules} {failureModule} />
<FlowGraphV2 {modules} {failureModule} />
<a
download="flow.json"
href={'data:text/json;charset=utf-8,' +