Workflow editor (#735)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
629
skyvern-frontend/package-lock.json
generated
629
skyvern-frontend/package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "skyvern-frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||
@@ -20,10 +23,14 @@
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@uiw/codemirror-theme-tokyo-night-storm": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@xyflow/react": "^12.1.1",
|
||||
"axios": "^1.6.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
@@ -99,6 +106,130 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.0.tgz",
|
||||
"integrity": "sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
|
||||
"integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
|
||||
"integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-python": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz",
|
||||
"integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.3.2",
|
||||
"@codemirror/language": "^6.8.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/python": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
|
||||
"integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.1.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz",
|
||||
"integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
|
||||
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
|
||||
"integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
|
||||
"integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.33.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.33.0.tgz",
|
||||
"integrity": "sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@dagrejs/dagre": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz",
|
||||
"integrity": "sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg==",
|
||||
"dependencies": {
|
||||
"@dagrejs/graphlib": "2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@dagrejs/graphlib": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
|
||||
"integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
|
||||
"engines": {
|
||||
"node": ">17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
@@ -774,6 +905,47 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
|
||||
"integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
|
||||
"integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
|
||||
"integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
|
||||
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/python": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz",
|
||||
"integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1699,6 +1871,182 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.0.tgz",
|
||||
"integrity": "sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-previous": "1.1.0",
|
||||
"@radix-ui/react-use-size": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
|
||||
"integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
|
||||
@@ -2196,6 +2544,49 @@
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
@@ -2428,6 +2819,86 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.0.tgz",
|
||||
"integrity": "sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": ">=6.0.0",
|
||||
"@codemirror/commands": ">=6.0.0",
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/lint": ">=6.0.0",
|
||||
"@codemirror/search": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-theme-tokyo-night-storm": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-tokyo-night-storm/-/codemirror-theme-tokyo-night-storm-4.23.0.tgz",
|
||||
"integrity": "sha512-+h827WKqSroE/9v3dIjLCVnlQnDBfsJUUS//UkWe8pnSnPM+5Qx28p4iAIsd6IDrRcgu5zduyEUESETHi75M8w==",
|
||||
"dependencies": {
|
||||
"@uiw/codemirror-themes": "4.23.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-themes": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.23.0.tgz",
|
||||
"integrity": "sha512-9fiji9xooZyBQozR1i6iTr56YP7j/Dr/VgsNWbqf5Szv+g+4WM1iZuiDGwNXmFMWX8gbkDzp6ASE21VCPSofWw==",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/react-codemirror": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.0.tgz",
|
||||
"integrity": "sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/theme-one-dark": "^6.0.0",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.23.0",
|
||||
"codemirror": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": ">=7.11.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0",
|
||||
"codemirror": ">=6.0.0",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||
@@ -2446,6 +2917,34 @@
|
||||
"vite": "^4 || ^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.1.1.tgz",
|
||||
"integrity": "sha512-0/ToFd7YY6fKAWdz+NstK5K190xONpdEBPGTWIcNT0CjiCZ6Wj8qRPlg4oxYHtKkTyBQfsEvBWayJaxhwDIOGw==",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.39",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.39.tgz",
|
||||
"integrity": "sha512-0WSdTCoA6kF/BqIFyUVinoK32Qj8scfLVxPdDahW1eYBKi5F1SYSouzVjpF2loYskyRbGl4KgxwaHiCgrBYFmA==",
|
||||
"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/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -2898,6 +3397,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"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/cli-cursor": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||
@@ -2937,6 +3441,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
|
||||
"integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -3028,6 +3546,11 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -3058,6 +3581,102 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -6229,6 +6848,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
|
||||
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||
@@ -6675,6 +7299,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"start": "npm run serve & npm run run-artifact-server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||
@@ -28,10 +31,14 @@
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@uiw/codemirror-theme-tokyo-night-storm": "^4.23.0",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"@xyflow/react": "^12.1.1",
|
||||
"axios": "^1.6.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
|
||||
@@ -113,7 +113,7 @@ export type ApiKeyApiResponse = {
|
||||
valid: boolean;
|
||||
};
|
||||
|
||||
export const WorkflowParameterType = {
|
||||
export const WorkflowParameterValueType = {
|
||||
String: "string",
|
||||
Integer: "integer",
|
||||
Float: "float",
|
||||
@@ -122,16 +122,28 @@ export const WorkflowParameterType = {
|
||||
FileURL: "file_url",
|
||||
} as const;
|
||||
|
||||
export type WorkflowParameterValueType =
|
||||
(typeof WorkflowParameterValueType)[keyof typeof WorkflowParameterValueType];
|
||||
|
||||
export const WorkflowParameterType = {
|
||||
Workflow: "workflow",
|
||||
Context: "context",
|
||||
Output: "output",
|
||||
AWS_Secret: "aws_secret",
|
||||
Bitwarden_Login_Credential: "bitwarden_login_credential",
|
||||
Bitwarden_Sensitive_Information: "bitwarden_sensitive_information",
|
||||
} as const;
|
||||
|
||||
export type WorkflowParameterType =
|
||||
(typeof WorkflowParameterType)[keyof typeof WorkflowParameterType];
|
||||
|
||||
export type WorkflowParameter = {
|
||||
workflow_parameter_id: string;
|
||||
workflow_parameter_type: WorkflowParameterType;
|
||||
key: string;
|
||||
description: string | null;
|
||||
workflow_parameter_id: string;
|
||||
parameter_type: WorkflowParameterType;
|
||||
workflow_parameter_type: WorkflowParameterValueType;
|
||||
workflow_id: string;
|
||||
parameter_type: "workflow"; // TODO other values
|
||||
default_value?: string;
|
||||
created_at: string | null;
|
||||
modified_at: string | null;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
type Props = React.ComponentProps<typeof Textarea>;
|
||||
|
||||
function AutoResizingTextarea(props: Props) {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// size the textarea correctly on first render
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
ref.current.style.height = `${ref.current.scrollHeight + 2}px`;
|
||||
}, []);
|
||||
|
||||
function setSize() {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
ref.current.style.height = "auto";
|
||||
ref.current.style.height = `${ref.current.scrollHeight + 2}px`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
{...props}
|
||||
onKeyDown={setSize}
|
||||
onInput={setSize}
|
||||
ref={ref}
|
||||
rows={1}
|
||||
className={cn("min-h-0 resize-none overflow-y-hidden", props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { AutoResizingTextarea };
|
||||
@@ -8,6 +8,7 @@ import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { toast } from "./ui/use-toast";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
|
||||
export type FileInputValue =
|
||||
| {
|
||||
@@ -36,8 +37,6 @@ function showFileSizeError() {
|
||||
function FileUpload({ value, onChange }: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [fileUrl, setFileUrl] = useState<string>("");
|
||||
const [highlight, setHighlight] = useState(false);
|
||||
const inputId = useId();
|
||||
|
||||
const uploadFileMutation = useMutation({
|
||||
@@ -92,37 +91,44 @@ function FileUpload({ value, onChange }: Props) {
|
||||
onChange(null);
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="w-1/2">
|
||||
const isManualUpload =
|
||||
typeof value === "object" && value !== null && file && "s3uri" in value;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className="h-36 w-full"
|
||||
defaultValue="upload"
|
||||
value={value === null ? undefined : isManualUpload ? "upload" : "fileURL"}
|
||||
onValueChange={(value) => {
|
||||
if (value === "upload") {
|
||||
onChange(null);
|
||||
} else {
|
||||
onChange("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="upload">Upload</TabsTrigger>
|
||||
<TabsTrigger value="fileURL">File URL</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="upload">
|
||||
{isManualUpload && ( // redundant check for ts compiler
|
||||
<div className="flex h-full items-center gap-4">
|
||||
<a href={value.presignedUrl} className="underline">
|
||||
<span>{file.name}</span>
|
||||
</a>
|
||||
<Button onClick={() => reset()}>Change</Button>
|
||||
</div>
|
||||
)}
|
||||
{value === null && (
|
||||
<Label
|
||||
htmlFor={inputId}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center justify-center border border-dashed py-8",
|
||||
{
|
||||
"border-slate-500": highlight,
|
||||
},
|
||||
"flex w-full cursor-pointer items-center justify-center border border-dashed py-8 hover:border-slate-500",
|
||||
)}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setHighlight(true);
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setHighlight(true);
|
||||
}}
|
||||
onDragLeave={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setHighlight(false);
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setHighlight(false);
|
||||
if (
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files.length > 0
|
||||
@@ -155,49 +161,21 @@ function FileUpload({ value, onChange }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center before:flex before:bg-slate-600 before:content-['']">
|
||||
OR
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="fileURL">
|
||||
<div className="space-y-2">
|
||||
<Label>File URL</Label>
|
||||
<div className="flex gap-2">
|
||||
{typeof value === "string" && (
|
||||
<Input
|
||||
value={fileUrl}
|
||||
onChange={(e) => setFileUrl(e.target.value)}
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(fileUrl);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<span>{value}</span>
|
||||
<Button onClick={() => reset()}>Change</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "object" && file && "s3uri" in value) {
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
<a href={value.presignedUrl} className="underline">
|
||||
<span>{file.name}</span>
|
||||
</a>
|
||||
<Button onClick={() => reset()}>Change</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
export { FileUpload };
|
||||
|
||||
27
skyvern-frontend/src/components/ui/switch.tsx
Normal file
27
skyvern-frontend/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
10
skyvern-frontend/src/hooks/useMountEffect.ts
Normal file
10
skyvern-frontend/src/hooks/useMountEffect.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
function useMountEffect(callback: () => void) {
|
||||
return useEffect(() => {
|
||||
callback();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
||||
|
||||
export { useMountEffect };
|
||||
@@ -70,7 +70,11 @@
|
||||
--input: 215.3 25% 26.7%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
--slate-elevation-2: 228 37% 11%;
|
||||
--slate-elevation-1: 228 45% 9%;
|
||||
--slate-elevation-2: 228 37% 10.6%;
|
||||
--slate-elevation-3: 227 30% 12%;
|
||||
--slate-elevation-4: 231 26% 14%;
|
||||
--slate-elevation-5: 230 22% 16%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { WorkflowPage } from "./routes/workflows/WorkflowPage";
|
||||
import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters";
|
||||
import { RetryTask } from "./routes/tasks/create/retry/RetryTask";
|
||||
import { WorkflowRun } from "./routes/workflows/WorkflowRun";
|
||||
import { WorkflowEditor } from "./routes/workflows/editor/WorkflowEditor";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -101,12 +102,16 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <WorkflowPage />,
|
||||
element: <WorkflowEditor />,
|
||||
},
|
||||
{
|
||||
path: "run",
|
||||
element: <WorkflowRunParameters />,
|
||||
},
|
||||
{
|
||||
path: "runs",
|
||||
element: <WorkflowPage />,
|
||||
},
|
||||
{
|
||||
path: ":workflowRunId",
|
||||
element: <WorkflowRun />,
|
||||
|
||||
38
skyvern-frontend/src/routes/root/Header.tsx
Normal file
38
skyvern-frontend/src/routes/root/Header.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DiscordLogoIcon } from "@radix-ui/react-icons";
|
||||
import GitHubButton from "react-github-btn";
|
||||
import { Link, useMatch } from "react-router-dom";
|
||||
|
||||
function Header() {
|
||||
const match = useMatch("/workflows/:workflowPermanentId");
|
||||
|
||||
if (match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<header>
|
||||
<div className="flex h-24 items-center justify-end gap-4 px-6">
|
||||
<Link
|
||||
to="https://discord.com/invite/fG2XXEuQX3"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<DiscordLogoIcon className="h-7 w-7" />
|
||||
</Link>
|
||||
<div className="h-7">
|
||||
<GitHubButton
|
||||
href="https://github.com/skyvern-ai/skyvern"
|
||||
data-color-scheme="no-preference: dark; light: dark; dark: dark;"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star skyvern-ai/skyvern on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export { Header };
|
||||
@@ -1,17 +1,13 @@
|
||||
import { Link, Outlet } from "react-router-dom";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { SideNav } from "./SideNav";
|
||||
import {
|
||||
DiscordLogoIcon,
|
||||
PinLeftIcon,
|
||||
PinRightIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { PinLeftIcon, PinRightIcon } from "@radix-ui/react-icons";
|
||||
import { Logo } from "@/components/Logo";
|
||||
import GitHubButton from "react-github-btn";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/util/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||
import { Header } from "./Header";
|
||||
|
||||
function RootLayout() {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
@@ -54,26 +50,7 @@ function RootLayout() {
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="flex h-24 items-center justify-end gap-4 px-6">
|
||||
<Link
|
||||
to="https://discord.com/invite/fG2XXEuQX3"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<DiscordLogoIcon className="h-7 w-7" />
|
||||
</Link>
|
||||
<div className="h-7">
|
||||
<GitHubButton
|
||||
href="https://github.com/skyvern-ai/skyvern"
|
||||
data-color-scheme="no-preference: dark; light: dark; dark: dark;"
|
||||
data-size="large"
|
||||
data-show-count="true"
|
||||
aria-label="Star skyvern-ai/skyvern on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
</div>
|
||||
</div>
|
||||
<Header />
|
||||
<main
|
||||
className={cn("pb-4 pl-64", {
|
||||
"pl-28": sidebarCollapsed,
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { WorkflowParameter } from "@/api/types";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -15,7 +8,15 @@ import { useParams } from "react-router-dom";
|
||||
import { WorkflowParameterInput } from "./WorkflowParameterInput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
type Props = {
|
||||
workflowParameters: Array<WorkflowParameter>;
|
||||
@@ -92,60 +93,91 @@ function RunWorkflowForm({ workflowParameters, initialValues }: Props) {
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{workflowParameters?.map((parameter) => {
|
||||
return (
|
||||
<FormField
|
||||
key={parameter.key}
|
||||
control={form.control}
|
||||
name={parameter.key}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (
|
||||
parameter.workflow_parameter_type === "json" &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return "Invalid JSON";
|
||||
}
|
||||
}
|
||||
},
|
||||
}}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{parameter.key}</FormLabel>
|
||||
<FormControl>
|
||||
<WorkflowParameterInput
|
||||
type={parameter.workflow_parameter_type}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
{parameter.description && (
|
||||
<FormDescription>
|
||||
{parameter.description}
|
||||
</FormDescription>
|
||||
)}
|
||||
{form.formState.errors[parameter.key] && (
|
||||
<div className="text-destructive">
|
||||
{form.formState.errors[parameter.key]?.message}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button type="submit" disabled={runWorkflowMutation.isPending}>
|
||||
{runWorkflowMutation.isPending && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Run workflow
|
||||
</Button>
|
||||
<Table>
|
||||
<TableHeader className="bg-slate-elevation2 text-slate-400 [&_tr]:border-b-0">
|
||||
<TableRow className="rounded-lg px-6 [&_th:first-child]:pl-6 [&_th]:py-4">
|
||||
<TableHead className="w-1/3 text-sm text-slate-400">
|
||||
Parameter Name
|
||||
</TableHead>
|
||||
<TableHead className="w-1/3 text-sm text-slate-400">
|
||||
Description
|
||||
</TableHead>
|
||||
<TableHead className="w-1/3 text-sm text-slate-400">
|
||||
Input
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{workflowParameters?.map((parameter) => {
|
||||
return (
|
||||
<FormField
|
||||
key={parameter.key}
|
||||
control={form.control}
|
||||
name={parameter.key}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (
|
||||
parameter.workflow_parameter_type === "json" &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return "Invalid JSON";
|
||||
}
|
||||
}
|
||||
},
|
||||
}}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TableRow className="[&_td:first-child]:pl-6 [&_td:last-child]:pr-6 [&_td]:py-4">
|
||||
<TableCell className="w-1/3">
|
||||
<div className="flex h-8 w-fit items-center rounded-sm bg-slate-elevation3 p-3">
|
||||
{parameter.key}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-1/3">
|
||||
<div>{parameter.description}</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-1/3">
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<WorkflowParameterInput
|
||||
type={parameter.workflow_parameter_type}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
{form.formState.errors[parameter.key] && (
|
||||
<div className="text-destructive">
|
||||
{
|
||||
form.formState.errors[parameter.key]
|
||||
?.message
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={runWorkflowMutation.isPending}>
|
||||
{runWorkflowMutation.isPending && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
{!runWorkflowMutation.isPending && (
|
||||
<PlayIcon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Run workflow
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,9 @@ function WorkflowPage() {
|
||||
)}
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link to="run">Create New Run</Link>
|
||||
<Link to={`/workflows/${workflowPermanentId}/run`}>
|
||||
Create New Run
|
||||
</Link>
|
||||
</Button>
|
||||
</header>
|
||||
<div className="space-y-4">
|
||||
@@ -124,7 +126,9 @@ function WorkflowPage() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
navigate(`${workflowRun.workflow_run_id}`);
|
||||
navigate(
|
||||
`/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}`,
|
||||
);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { WorkflowParameterType } from "@/api/types";
|
||||
import { WorkflowParameterValueType } from "@/api/types";
|
||||
import { FileInputValue, FileUpload } from "@/components/FileUpload";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { CodeEditor } from "./components/CodeEditor";
|
||||
|
||||
type Props = {
|
||||
type: WorkflowParameterType;
|
||||
type: WorkflowParameterValueType;
|
||||
value: unknown;
|
||||
onChange: (value: unknown) => void;
|
||||
};
|
||||
|
||||
function WorkflowParameterInput({ type, value, onChange }: Props) {
|
||||
if (type === "json" || type === "string") {
|
||||
if (type === "json") {
|
||||
return (
|
||||
<Textarea
|
||||
<CodeEditor
|
||||
language="json"
|
||||
onChange={(value) => onChange(value)}
|
||||
value={
|
||||
typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
||||
}
|
||||
fontSize={12}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "string") {
|
||||
return (
|
||||
<Input
|
||||
value={value as string}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { WorkflowApiResponse, WorkflowParameterType } from "@/api/types";
|
||||
import { WorkflowApiResponse, WorkflowParameterValueType } from "@/api/types";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { RunWorkflowForm } from "./RunWorkflowForm";
|
||||
|
||||
function defaultValue(type: WorkflowParameterType) {
|
||||
function defaultValue(type: WorkflowParameterValueType) {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return "";
|
||||
@@ -76,8 +76,12 @@ function WorkflowRunParameters() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<header>
|
||||
<h1 className="text-2xl font-semibold">Workflow Run Parameters</h1>
|
||||
<header className="space-y-5">
|
||||
<h1 className="text-3xl">Run Parameters</h1>
|
||||
<h2 className="text-lg text-slate-400">
|
||||
Fill the placeholder values that you have linked throughout your
|
||||
workflow.
|
||||
</h2>
|
||||
</header>
|
||||
<RunWorkflowForm
|
||||
initialValues={initialValues}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { WorkflowApiResponse, WorkflowRunApiResponse } from "@/api/types";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@@ -17,9 +18,20 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
import { cn } from "@/util/utils";
|
||||
import {
|
||||
CounterClockwiseClockIcon,
|
||||
Pencil2Icon,
|
||||
PlayIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard";
|
||||
@@ -80,9 +92,10 @@ function Workflows() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-1/3">ID</TableHead>
|
||||
<TableHead className="w-1/3">Title</TableHead>
|
||||
<TableHead className="w-1/3">Created At</TableHead>
|
||||
<TableHead className="w-1/4">ID</TableHead>
|
||||
<TableHead className="w-1/4">Status</TableHead>
|
||||
<TableHead className="w-1/4">Created At</TableHead>
|
||||
<TableHead className="w-1/4"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -97,22 +110,7 @@ function Workflows() {
|
||||
) : (
|
||||
workflows?.map((workflow) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={workflow.workflow_permanent_id}
|
||||
onClick={(event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(
|
||||
window.location.origin +
|
||||
`/workflows/${workflow.workflow_permanent_id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
return;
|
||||
}
|
||||
navigate(`${workflow.workflow_permanent_id}`);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<TableRow key={workflow.workflow_permanent_id}>
|
||||
<TableCell className="w-1/3">
|
||||
{workflow.workflow_permanent_id}
|
||||
</TableCell>
|
||||
@@ -120,6 +118,64 @@ function Workflows() {
|
||||
<TableCell className="w-1/3">
|
||||
{basicTimeFormat(workflow.created_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/workflows/${workflow.workflow_permanent_id}/runs`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View Past Runs</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/workflows/${workflow.workflow_permanent_id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Pencil2Icon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Open in Editor</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/workflows/${workflow.workflow_permanent_id}/run`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Create New Run</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { cn } from "@/util/utils";
|
||||
import { Outlet, useMatch } from "react-router-dom";
|
||||
|
||||
function WorkflowsPageLayout() {
|
||||
const match = useMatch("/workflows/:workflowPermanentId");
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-8">
|
||||
<main
|
||||
className={cn({
|
||||
"container mx-auto px-8": !match,
|
||||
})}
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { tokyoNightStorm } from "@uiw/codemirror-theme-tokyo-night-storm";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
language: "python" | "json";
|
||||
disabled?: boolean;
|
||||
minHeight?: string;
|
||||
className?: string;
|
||||
fontSize?: number;
|
||||
};
|
||||
|
||||
function CodeEditor({
|
||||
value,
|
||||
onChange,
|
||||
minHeight,
|
||||
language,
|
||||
className,
|
||||
fontSize = 8,
|
||||
}: Props) {
|
||||
const extensions = language === "json" ? [json()] : [python()];
|
||||
return (
|
||||
<CodeMirror
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
extensions={extensions}
|
||||
theme={tokyoNightStorm}
|
||||
minHeight={minHeight}
|
||||
className={cn("cursor-auto", className)}
|
||||
style={{
|
||||
fontSize: fontSize,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { CodeEditor };
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CodeEditor } from "./CodeEditor";
|
||||
|
||||
type Props = {
|
||||
value: Record<string, unknown> | null;
|
||||
onChange: (value: Record<string, unknown> | null) => void;
|
||||
};
|
||||
|
||||
function DataSchema({ value, onChange }: Props) {
|
||||
if (value === null) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Data Schema</Label>
|
||||
<Checkbox
|
||||
checked={false}
|
||||
onCheckedChange={() => {
|
||||
onChange({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Data Schema</Label>
|
||||
<Checkbox
|
||||
checked
|
||||
onCheckedChange={() => {
|
||||
onChange(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={JSON.stringify(value, null, 2)}
|
||||
onChange={() => {
|
||||
// TODO
|
||||
}}
|
||||
className="nowheel nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DataSchema };
|
||||
107
skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx
Normal file
107
skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
Edge,
|
||||
Panel,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesInitialized,
|
||||
useNodesState,
|
||||
} from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { WorkflowHeader } from "./WorkflowHeader";
|
||||
import { AppNode, nodeTypes } from "./nodes";
|
||||
import "./reactFlowOverrideStyles.css";
|
||||
import { layout } from "./workflowEditorUtils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
initialNodes: Array<AppNode>;
|
||||
initialEdges: Array<Edge>;
|
||||
};
|
||||
|
||||
function FlowRenderer({ title, initialEdges, initialNodes }: Props) {
|
||||
const [rightSidePanelOpen, setRightSidePanelOpen] = useState(false);
|
||||
const [rightSidePanelContent, setRightSidePanelContent] = useState<
|
||||
"parameters" | "nodeLibrary" | null
|
||||
>(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const nodesInitialized = useNodesInitialized();
|
||||
|
||||
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
||||
const layoutedElements = layout(nodes, edges);
|
||||
setNodes(layoutedElements.nodes);
|
||||
setEdges(layoutedElements.edges);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (nodesInitialized) {
|
||||
doLayout(nodes, edges);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodesInitialized]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={(changes) => {
|
||||
const dimensionChanges = changes.filter(
|
||||
(change) => change.type === "dimensions",
|
||||
);
|
||||
const tempNodes = [...nodes];
|
||||
dimensionChanges.forEach((change) => {
|
||||
const node = tempNodes.find((node) => node.id === change.id);
|
||||
if (node) {
|
||||
if (node.measured?.width) {
|
||||
node.measured.width = change.dimensions?.width;
|
||||
}
|
||||
if (node.measured?.height) {
|
||||
node.measured.height = change.dimensions?.height;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (dimensionChanges.length > 0) {
|
||||
doLayout(tempNodes, edges);
|
||||
}
|
||||
onNodesChange(changes);
|
||||
}}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
colorMode="dark"
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
maxZoom: 1,
|
||||
}}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||
<Controls position="bottom-left" />
|
||||
<Panel position="top-center" className="h-20">
|
||||
<WorkflowHeader
|
||||
title={title}
|
||||
parametersPanelOpen={rightSidePanelOpen}
|
||||
onParametersClick={() => {
|
||||
setRightSidePanelOpen((open) => !open);
|
||||
setRightSidePanelContent("parameters");
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
{rightSidePanelOpen && (
|
||||
<Panel
|
||||
position="top-right"
|
||||
className="w-96 rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
|
||||
>
|
||||
{rightSidePanelContent === "parameters" && (
|
||||
<WorkflowParametersPanel />
|
||||
)}
|
||||
</Panel>
|
||||
)}
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
|
||||
export { FlowRenderer };
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
type LayoutCallbackFunction = () => void;
|
||||
|
||||
const LayoutCallbackContext = createContext<LayoutCallbackFunction | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export { LayoutCallbackContext };
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||
import { FlowRenderer } from "./FlowRenderer";
|
||||
import { getElements } from "./workflowEditorUtils";
|
||||
|
||||
function WorkflowEditor() {
|
||||
const { workflowPermanentId } = useParams();
|
||||
|
||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
|
||||
// TODO
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!workflow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elements = getElements(workflow.workflow_definition.blocks);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full">
|
||||
<ReactFlowProvider>
|
||||
<FlowRenderer
|
||||
title={workflow.title}
|
||||
initialNodes={elements.nodes}
|
||||
initialEdges={elements.edges}
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowEditor };
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
ExitIcon,
|
||||
PlayIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
parametersPanelOpen: boolean;
|
||||
onParametersClick: () => void;
|
||||
};
|
||||
|
||||
function WorkflowHeader({
|
||||
title,
|
||||
parametersPanelOpen,
|
||||
onParametersClick,
|
||||
}: Props) {
|
||||
const { workflowPermanentId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full bg-slate-elevation2">
|
||||
<div className="flex h-full w-1/3 items-center pl-6">
|
||||
<div
|
||||
className="cursor-pointer rounded-full p-2 hover:bg-slate-elevation5"
|
||||
onClick={() => {
|
||||
navigate("/workflows");
|
||||
}}
|
||||
>
|
||||
<ExitIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-1/3 items-center justify-center">
|
||||
<span className="max-w-max truncate text-3xl">{title}</span>
|
||||
</div>
|
||||
<div className="flex h-full w-1/3 items-center justify-end gap-4 p-4">
|
||||
<Button variant="secondary" size="lg" onClick={onParametersClick}>
|
||||
<span className="mr-2">Parameters</span>
|
||||
{parametersPanelOpen ? (
|
||||
<ChevronUpIcon className="h-6 w-6" />
|
||||
) : (
|
||||
<ChevronDownIcon className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
navigate(`/workflows/${workflowPermanentId}/run`);
|
||||
}}
|
||||
>
|
||||
<span className="mr-2">Run</span>
|
||||
<PlayIcon className="h-6 w-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowHeader };
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { CodeBlockNode } from "./types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CodeIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
|
||||
function CodeBlockNode({ data }: NodeProps<CodeBlockNode>) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<CodeIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Task Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Code Input</Label>
|
||||
<CodeEditor
|
||||
language="python"
|
||||
value={data.code}
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CodeBlockNode };
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type CodeBlockNodeData = {
|
||||
code: string;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type CodeBlockNode = Node<CodeBlockNodeData, "codeBlock">;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { DotsHorizontalIcon, DownloadIcon } from "@radix-ui/react-icons";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { DownloadNode } from "./types";
|
||||
|
||||
function DownloadNode({ data }: NodeProps<DownloadNode>) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<DownloadIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Download Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-slate-400">File URL</Label>
|
||||
<Input
|
||||
value={data.url}
|
||||
onChange={() => {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
}}
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DownloadNode };
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type DownloadNodeData = {
|
||||
url: string;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type DownloadNode = Node<DownloadNodeData, "download">;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { FileParserNode } from "./types";
|
||||
import { CursorTextIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
function FileParserNode({ data }: NodeProps<FileParserNode>) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<CursorTextIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">File Parser Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm text-slate-400">File URL</span>
|
||||
<Input
|
||||
value={data.fileUrl}
|
||||
onChange={() => {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
}}
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { FileParserNode };
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type FileParserNodeData = {
|
||||
fileUrl: string;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type FileParserNode = Node<FileParserNodeData, "fileParser">;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { DotsHorizontalIcon, UpdateIcon } from "@radix-ui/react-icons";
|
||||
import { Handle, NodeProps, Position, useNodes } from "@xyflow/react";
|
||||
import type { LoopNode } from "./types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
const nodes = useNodes();
|
||||
const children = nodes.filter((node) => node.parentId === id);
|
||||
const furthestDownChild: Node | null = children.reduce(
|
||||
(acc, child) => {
|
||||
if (!acc) {
|
||||
return child;
|
||||
}
|
||||
if (child.position.y > acc.position.y) {
|
||||
return child;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
null as Node | null,
|
||||
);
|
||||
|
||||
const childrenHeightExtent =
|
||||
(furthestDownChild?.measured?.height ?? 0) +
|
||||
(furthestDownChild?.position.y ?? 0) +
|
||||
24;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div
|
||||
className="w-[60rem] rounded-md border-2 border-dashed border-slate-600 p-2"
|
||||
style={{
|
||||
height: childrenHeightExtent,
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<UpdateIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Loop Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Loop Value</Label>
|
||||
<Input
|
||||
value={data.loopValue}
|
||||
onChange={() => {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
}}
|
||||
placeholder="What value are you iterating over?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoopNode };
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type LoopNodeData = {
|
||||
loopValue: string;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { SendEmailNode } from "./types";
|
||||
import { DotsHorizontalIcon, EnvelopeClosedIcon } from "@radix-ui/react-icons";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
function SendEmailNode({ data }: NodeProps<SendEmailNode>) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<EnvelopeClosedIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Send Email Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Recipient</Label>
|
||||
<Input
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.recipients.join(", ")}
|
||||
placeholder="example@gmail.com"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Subject</Label>
|
||||
<Input
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.subject}
|
||||
placeholder="What is the gist?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Body</Label>
|
||||
<Input
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.body}
|
||||
placeholder="What would you like to say?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">File Attachments</Label>
|
||||
<Input
|
||||
value={data.fileAttachments?.join(", ") ?? ""}
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center gap-10">
|
||||
<Label className="text-xs text-slate-300">
|
||||
Attach all downloaded files
|
||||
</Label>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SendEmailNode };
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type SendEmailNodeData = {
|
||||
recipients: string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
fileAttachments: string[] | null;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type SendEmailNode = Node<SendEmailNodeData, "sendEmail">;
|
||||
@@ -0,0 +1,220 @@
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { DotsHorizontalIcon, ListBulletIcon } from "@radix-ui/react-icons";
|
||||
import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch";
|
||||
import type { TaskNodeDisplayMode } from "./types";
|
||||
import type { TaskNode } from "./types";
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { DataSchema } from "../../../components/DataSchema";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { TaskNodeErrorMapping } from "./TaskNodeErrorMapping";
|
||||
|
||||
function TaskNode({ data }: NodeProps<TaskNode>) {
|
||||
const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>("basic");
|
||||
const { editable } = data;
|
||||
|
||||
const basicContent = (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">URL</Label>
|
||||
<AutoResizingTextarea
|
||||
value={data.url}
|
||||
className="nopan"
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
placeholder="https://"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Goal</Label>
|
||||
<AutoResizingTextarea
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.navigationGoal}
|
||||
placeholder="What are you looking to do?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const advancedContent = (
|
||||
<>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
defaultValue={["content", "extraction", "limits"]}
|
||||
>
|
||||
<AccordionItem value="content">
|
||||
<AccordionTrigger>Content</AccordionTrigger>
|
||||
<AccordionContent className="pl-[1.5rem] pr-1">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">URL</Label>
|
||||
<AutoResizingTextarea
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.url}
|
||||
placeholder="https://"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Goal</Label>
|
||||
<AutoResizingTextarea
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.navigationGoal}
|
||||
placeholder="What are you looking to do?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="extraction">
|
||||
<AccordionTrigger>Extraction</AccordionTrigger>
|
||||
<AccordionContent className="pl-[1.5rem] pr-1">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">
|
||||
Data Extraction Goal
|
||||
</Label>
|
||||
<AutoResizingTextarea
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.dataExtractionGoal}
|
||||
placeholder="What outputs are you looking to get?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<DataSchema
|
||||
value={data.dataSchema}
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="limits">
|
||||
<AccordionTrigger>Limits</AccordionTrigger>
|
||||
<AccordionContent className="pl-[1.5rem] pr-1">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Retries
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
className="nopan w-44"
|
||||
value={data.maxRetries ?? 0}
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Steps Override
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
className="nopan w-44"
|
||||
value={data.maxStepsOverride ?? 0}
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Allow Downloads
|
||||
</Label>
|
||||
<div className="w-44">
|
||||
<Switch
|
||||
checked={data.allowDownloads}
|
||||
onCheckedChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TaskNodeErrorMapping
|
||||
value={data.errorCodeMapping}
|
||||
onChange={() => {
|
||||
if (!editable) return;
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<ListBulletIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Task Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<TaskNodeDisplayModeSwitch
|
||||
value={displayMode}
|
||||
onChange={setDisplayMode}
|
||||
/>
|
||||
{displayMode === "basic" && basicContent}
|
||||
{displayMode === "advanced" && advancedContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskNode };
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cn } from "@/util/utils";
|
||||
import { TaskNodeDisplayMode } from "./types";
|
||||
|
||||
type Props = {
|
||||
value: TaskNodeDisplayMode;
|
||||
onChange: (mode: TaskNodeDisplayMode) => void;
|
||||
};
|
||||
|
||||
function TaskNodeDisplayModeSwitch({ value, onChange }: Props) {
|
||||
return (
|
||||
<div className="flex w-fit gap-1 rounded-sm border border-slate-700 p-2">
|
||||
<div
|
||||
className={cn("cursor-pointer rounded-sm p-2 hover:bg-slate-700", {
|
||||
"bg-slate-700": value === "basic",
|
||||
})}
|
||||
onClick={() => {
|
||||
onChange("basic");
|
||||
}}
|
||||
>
|
||||
Basic
|
||||
</div>
|
||||
<div
|
||||
className={cn("cursor-pointer rounded-sm p-2 hover:bg-slate-700", {
|
||||
"bg-slate-700": value === "advanced",
|
||||
})}
|
||||
onClick={() => {
|
||||
onChange("advanced");
|
||||
}}
|
||||
>
|
||||
Advanced
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskNodeDisplayModeSwitch };
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
|
||||
type Props = {
|
||||
value: Record<string, unknown> | null;
|
||||
onChange: (value: Record<string, unknown> | null) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
function TaskNodeErrorMapping({ value, onChange, disabled }: Props) {
|
||||
if (value === null) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={false}
|
||||
disabled={disabled}
|
||||
onCheckedChange={() => {
|
||||
onChange({});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked
|
||||
disabled={disabled}
|
||||
onCheckedChange={() => {
|
||||
onChange(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={JSON.stringify(value, null, 2)}
|
||||
disabled={disabled}
|
||||
onChange={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
}}
|
||||
className="nowheel nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TaskNodeErrorMapping };
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type TaskNodeData = {
|
||||
url: string;
|
||||
navigationGoal: string;
|
||||
dataExtractionGoal: string;
|
||||
errorCodeMapping: Record<string, string> | null;
|
||||
dataSchema: Record<string, unknown> | null;
|
||||
maxRetries: number | null;
|
||||
maxStepsOverride: number | null;
|
||||
allowDownloads: boolean;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type TaskNode = Node<TaskNodeData, "task">;
|
||||
|
||||
export type TaskNodeDisplayMode = "basic" | "advanced";
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CursorTextIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { TextPromptNode } from "./types";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { DataSchema } from "@/routes/workflows/components/DataSchema";
|
||||
|
||||
function TextPromptNode({ data }: NodeProps<TextPromptNode>) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<CursorTextIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Text Prompt Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-slate-300">Prompt</Label>
|
||||
<AutoResizingTextarea
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
value={data.prompt}
|
||||
placeholder="What do you want to generate?"
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<DataSchema
|
||||
value={data.jsonSchema}
|
||||
onChange={() => {
|
||||
if (!data.editable) return;
|
||||
// TODO
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TextPromptNode };
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type TextPromptNodeData = {
|
||||
prompt: string;
|
||||
jsonSchema: Record<string, unknown> | null;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type TextPromptNode = Node<TextPromptNodeData, "textPrompt">;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
||||
import type { UploadNode } from "./types";
|
||||
import { DotsHorizontalIcon, UploadIcon } from "@radix-ui/react-icons";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
function UploadNode({ data }: NodeProps<UploadNode>) {
|
||||
return (
|
||||
<div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="a"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
id="b"
|
||||
className="opacity-0"
|
||||
/>
|
||||
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||
<div className="flex h-[2.75rem] justify-between">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||
<UploadIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
||||
<span className="text-xs text-slate-400">Upload Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DotsHorizontalIcon className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-slate-400">File Path</Label>
|
||||
<Input
|
||||
value={data.path}
|
||||
onChange={() => {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
// TODO
|
||||
}}
|
||||
className="nopan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { UploadNode };
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
|
||||
export type UploadNodeData = {
|
||||
path: string;
|
||||
editable: boolean;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type UploadNode = Node<UploadNodeData, "upload">;
|
||||
38
skyvern-frontend/src/routes/workflows/editor/nodes/index.ts
Normal file
38
skyvern-frontend/src/routes/workflows/editor/nodes/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { memo } from "react";
|
||||
import { CodeBlockNode as CodeBlockNodeComponent } from "./CodeBlockNode/CodeBlockNode";
|
||||
import { CodeBlockNode } from "./CodeBlockNode/types";
|
||||
import { LoopNode as LoopNodeComponent } from "./LoopNode/LoopNode";
|
||||
import type { LoopNode } from "./LoopNode/types";
|
||||
import { SendEmailNode as SendEmailNodeComponent } from "./SendEmailNode/SendEmailNode";
|
||||
import type { SendEmailNode } from "./SendEmailNode/types";
|
||||
import { TaskNode as TaskNodeComponent } from "./TaskNode/TaskNode";
|
||||
import type { TaskNode } from "./TaskNode/types";
|
||||
import { TextPromptNode as TextPromptNodeComponent } from "./TextPromptNode/TextPromptNode";
|
||||
import type { TextPromptNode } from "./TextPromptNode/types";
|
||||
import type { FileParserNode } from "./FileParserNode/types";
|
||||
import { FileParserNode as FileParserNodeComponent } from "./FileParserNode/FileParserNode";
|
||||
import type { UploadNode } from "./UploadNode/types";
|
||||
import { UploadNode as UploadNodeComponent } from "./UploadNode/UploadNode";
|
||||
import type { DownloadNode } from "./DownloadNode/types";
|
||||
import { DownloadNode as DownloadNodeComponent } from "./DownloadNode/DownloadNode";
|
||||
|
||||
export type AppNode =
|
||||
| LoopNode
|
||||
| TaskNode
|
||||
| TextPromptNode
|
||||
| SendEmailNode
|
||||
| CodeBlockNode
|
||||
| FileParserNode
|
||||
| UploadNode
|
||||
| DownloadNode;
|
||||
|
||||
export const nodeTypes = {
|
||||
loop: memo(LoopNodeComponent),
|
||||
task: memo(TaskNodeComponent),
|
||||
textPrompt: memo(TextPromptNodeComponent),
|
||||
sendEmail: memo(SendEmailNodeComponent),
|
||||
codeBlock: memo(CodeBlockNodeComponent),
|
||||
fileParser: memo(FileParserNodeComponent),
|
||||
upload: memo(UploadNodeComponent),
|
||||
download: memo(DownloadNodeComponent),
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useWorkflowQuery } from "../../hooks/useWorkflowQuery";
|
||||
|
||||
function WorkflowParametersPanel() {
|
||||
const { workflowPermanentId } = useParams();
|
||||
|
||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
|
||||
if (isLoading || !workflow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workflowParameters = workflow.workflow_definition.parameters.filter(
|
||||
(parameter) => parameter.parameter_type === "workflow",
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<header>
|
||||
<h1 className="text-lg">Workflow Parameters</h1>
|
||||
<span className="text-sm text-slate-400">
|
||||
Create placeholder values that you can link in nodes. You will be
|
||||
prompted to fill them in before running your workflow.
|
||||
</span>
|
||||
</header>
|
||||
<section className="space-y-2">
|
||||
{workflowParameters.map((parameter) => {
|
||||
return (
|
||||
<div
|
||||
key={parameter.key}
|
||||
className="flex items-center gap-4 rounded-md bg-slate-elevation1 px-3 py-2"
|
||||
>
|
||||
<span className="text-sm">{parameter.key}</span>
|
||||
<span className="text-sm text-slate-400">
|
||||
{parameter.workflow_parameter_type}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowParametersPanel };
|
||||
@@ -0,0 +1,23 @@
|
||||
.react-flow__panel.top.center {
|
||||
margin: 0;
|
||||
top: 2rem;
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
|
||||
.react-flow__panel.top.right {
|
||||
margin: 0;
|
||||
top: 7.75rem;
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.react-flow__node-regular {
|
||||
@apply bg-slate-elevation3;
|
||||
}
|
||||
|
||||
.react-flow__handle-top {
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.react-flow__handle-bottom {
|
||||
bottom: 3px;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { Edge } from "@xyflow/react";
|
||||
import { AppNode } from "./nodes";
|
||||
import Dagre from "@dagrejs/dagre";
|
||||
import type { WorkflowBlock } from "../types/workflowTypes";
|
||||
|
||||
function layoutUtil(
|
||||
nodes: Array<AppNode>,
|
||||
edges: Array<Edge>,
|
||||
options: Dagre.configUnion = {},
|
||||
): { nodes: Array<AppNode>; edges: Array<Edge> } {
|
||||
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: "TB", ...options });
|
||||
|
||||
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
|
||||
nodes.forEach((node) =>
|
||||
g.setNode(node.id, {
|
||||
...node,
|
||||
width: node.measured?.width ?? 0,
|
||||
height: node.measured?.height ?? 0,
|
||||
}),
|
||||
);
|
||||
|
||||
Dagre.layout(g);
|
||||
|
||||
return {
|
||||
nodes: nodes.map((node) => {
|
||||
const dagreNode = g.node(node.id);
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the React Flow node anchor point (top left).
|
||||
const x = dagreNode.x - (node.measured?.width ?? 0) / 2;
|
||||
const y = dagreNode.y - (node.measured?.height ?? 0) / 2;
|
||||
|
||||
return { ...node, position: { x, y } };
|
||||
}),
|
||||
edges,
|
||||
};
|
||||
}
|
||||
|
||||
function layout(
|
||||
nodes: Array<AppNode>,
|
||||
edges: Array<Edge>,
|
||||
): { nodes: Array<AppNode>; edges: Array<Edge> } {
|
||||
const loopNodes = nodes.filter(
|
||||
(node) => node.type === "loop" && !node.parentId,
|
||||
);
|
||||
const loopNodeChildren: Array<Array<AppNode>> = loopNodes.map(() => []);
|
||||
|
||||
loopNodes.forEach((node, index) => {
|
||||
const childNodes = nodes.filter((n) => n.parentId === node.id);
|
||||
const childEdges = edges.filter((edge) =>
|
||||
childNodes.some(
|
||||
(node) => node.id === edge.source || node.id === edge.target,
|
||||
),
|
||||
);
|
||||
const layouted = layoutUtil(childNodes, childEdges, {
|
||||
marginx: 240,
|
||||
marginy: 200,
|
||||
});
|
||||
loopNodeChildren[index] = layouted.nodes;
|
||||
});
|
||||
|
||||
const topLevelNodes = nodes.filter((node) => !node.parentId);
|
||||
|
||||
const topLevelNodesLayout = layoutUtil(topLevelNodes, edges);
|
||||
|
||||
return {
|
||||
nodes: topLevelNodesLayout.nodes.concat(loopNodeChildren.flat()),
|
||||
edges,
|
||||
};
|
||||
}
|
||||
|
||||
function convertToNode(
|
||||
identifiers: { id: string; parentId?: string },
|
||||
block: WorkflowBlock,
|
||||
): AppNode {
|
||||
const common = {
|
||||
draggable: false,
|
||||
};
|
||||
switch (block.block_type) {
|
||||
case "task": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "task",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
url: block.url ?? "",
|
||||
navigationGoal: block.navigation_goal ?? "",
|
||||
dataExtractionGoal: block.data_extraction_goal ?? "",
|
||||
dataSchema: block.data_schema ?? null,
|
||||
errorCodeMapping: block.error_code_mapping ?? null,
|
||||
allowDownloads: block.complete_on_download ?? false,
|
||||
maxRetries: block.max_retries ?? null,
|
||||
maxStepsOverride: block.max_steps_per_run ?? null,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
case "code": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "codeBlock",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
code: block.code,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
case "send_email": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "sendEmail",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
body: block.body,
|
||||
fileAttachments: block.file_attachments,
|
||||
recipients: block.recipients,
|
||||
subject: block.subject,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
case "text_prompt": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "textPrompt",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
prompt: block.prompt,
|
||||
jsonSchema: block.json_schema ?? null,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
case "for_loop": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "loop",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
loopValue: block.loop_over.key,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
case "file_url_parser": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "fileParser",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
fileUrl: block.file_url,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
case "download_to_s3": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "download",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
url: block.url,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
case "upload_to_s3": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "upload",
|
||||
data: {
|
||||
label: block.label,
|
||||
editable: false,
|
||||
path: block.path,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getElements(
|
||||
blocks: Array<WorkflowBlock>,
|
||||
parentId?: string,
|
||||
): { nodes: Array<AppNode>; edges: Array<Edge> } {
|
||||
const nodes: Array<AppNode> = [];
|
||||
const edges: Array<Edge> = [];
|
||||
|
||||
blocks.forEach((block, index) => {
|
||||
const id = parentId ? `${parentId}-${index}` : String(index);
|
||||
const nextId = parentId ? `${parentId}-${index + 1}` : String(index + 1);
|
||||
nodes.push(convertToNode({ id, parentId }, block));
|
||||
if (block.block_type === "for_loop") {
|
||||
const subElements = getElements(block.loop_blocks, id);
|
||||
nodes.push(...subElements.nodes);
|
||||
edges.push(...subElements.edges);
|
||||
}
|
||||
if (index !== blocks.length - 1) {
|
||||
edges.push({
|
||||
id: `edge-${id}-${nextId}`,
|
||||
source: id,
|
||||
target: nextId,
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
export { getElements, layout };
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { WorkflowApiResponse } from "../types/workflowTypes";
|
||||
|
||||
type Props = {
|
||||
workflowPermanentId?: string;
|
||||
};
|
||||
|
||||
function useWorkflowQuery({ workflowPermanentId }: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
return useQuery<WorkflowApiResponse>({
|
||||
queryKey: ["workflow", workflowPermanentId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
return client
|
||||
.get(`/workflows/${workflowPermanentId}`)
|
||||
.then((response) => response.data);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { useWorkflowQuery };
|
||||
217
skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
Normal file
217
skyvern-frontend/src/routes/workflows/types/workflowTypes.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
export type WorkflowParameterBase = {
|
||||
parameter_type: WorkflowParameterType;
|
||||
key: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export type AWSSecretParameter = WorkflowParameterBase & {
|
||||
parameter_type: "aws_secret";
|
||||
workflow_id: string;
|
||||
aws_secret_parameter_id: string;
|
||||
aws_key: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
};
|
||||
|
||||
export type BitwardenLoginCredentialParameter = WorkflowParameterBase & {
|
||||
parameter_type: "bitwarden_login_credential";
|
||||
workflow_id: string;
|
||||
bitwarden_login_credential_parameter_id: string;
|
||||
bitwarden_client_id_aws_secret_key: string;
|
||||
bitwarden_client_secret_aws_secret_key: string;
|
||||
bitwarden_master_password_aws_secret_key: string;
|
||||
bitwarden_collection_id: string;
|
||||
url_parameter_key: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
};
|
||||
|
||||
export type BitwardenSensitiveInformationParameter = WorkflowParameterBase & {
|
||||
parameter_type: "bitwarden_sensitive_information";
|
||||
workflow_id: string;
|
||||
bitwarden_sensitive_information_parameter_id: string;
|
||||
bitwarden_client_id_aws_secret_key: string;
|
||||
bitwarden_client_secret_aws_secret_key: string;
|
||||
bitwarden_master_password_aws_secret_key: string;
|
||||
bitwarden_collection_id: string;
|
||||
bitwarden_identity_key: string;
|
||||
bitwarden_identity_fields: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowParameter = WorkflowParameterBase & {
|
||||
parameter_type: "workflow";
|
||||
workflow_id: string;
|
||||
workflow_parameter_id: string;
|
||||
workflow_parameter_type: WorkflowParameterValueType;
|
||||
default_value: unknown;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
};
|
||||
|
||||
export type ContextParameter = WorkflowParameterBase & {
|
||||
parameter_type: "context";
|
||||
source: WorkflowParameter;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
export type OutputParameter = WorkflowParameterBase & {
|
||||
parameter_type: "output";
|
||||
output_parameter_id: string;
|
||||
workflow_id: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
};
|
||||
|
||||
export const WorkflowParameterValueType = {
|
||||
String: "string",
|
||||
Integer: "integer",
|
||||
Float: "float",
|
||||
Boolean: "boolean",
|
||||
JSON: "json",
|
||||
FileURL: "file_url",
|
||||
} as const;
|
||||
|
||||
export type WorkflowParameterValueType =
|
||||
(typeof WorkflowParameterValueType)[keyof typeof WorkflowParameterValueType];
|
||||
|
||||
export const WorkflowParameterType = {
|
||||
Workflow: "workflow",
|
||||
Context: "context",
|
||||
Output: "output",
|
||||
AWS_Secret: "aws_secret",
|
||||
Bitwarden_Login_Credential: "bitwarden_login_credential",
|
||||
Bitwarden_Sensitive_Information: "bitwarden_sensitive_information",
|
||||
} as const;
|
||||
|
||||
export type WorkflowParameterType =
|
||||
(typeof WorkflowParameterType)[keyof typeof WorkflowParameterType];
|
||||
|
||||
export type Parameter =
|
||||
| WorkflowParameter
|
||||
| OutputParameter
|
||||
| ContextParameter
|
||||
| BitwardenLoginCredentialParameter
|
||||
| BitwardenSensitiveInformationParameter
|
||||
| AWSSecretParameter;
|
||||
|
||||
export const WorkflowBlockType = {
|
||||
Task: "task",
|
||||
ForLoop: "for_loop",
|
||||
Code: "code",
|
||||
TextPrompt: "text_prompt",
|
||||
DownloadToS3: "download_to_s3",
|
||||
UploadToS3: "upload_to_s3",
|
||||
SendEmail: "send_email",
|
||||
FileURLParser: "file_url_parser",
|
||||
};
|
||||
|
||||
export type WorkflowBlockType =
|
||||
(typeof WorkflowBlockType)[keyof typeof WorkflowBlockType];
|
||||
|
||||
export type WorkflowBlockBase = {
|
||||
label: string;
|
||||
block_type: WorkflowBlockType;
|
||||
output_parameter: OutputParameter;
|
||||
continue_on_failure: boolean;
|
||||
};
|
||||
|
||||
export type TaskBlock = WorkflowBlockBase & {
|
||||
block_type: "task";
|
||||
url: string | null;
|
||||
title: string;
|
||||
navigation_goal: string | null;
|
||||
data_extraction_goal: string | null;
|
||||
data_schema: Record<string, unknown> | null;
|
||||
error_code_mapping: Record<string, string> | null;
|
||||
max_retries?: number;
|
||||
max_steps_per_run?: number | null;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
complete_on_download?: boolean;
|
||||
};
|
||||
|
||||
export type ForLoopBlock = WorkflowBlockBase & {
|
||||
block_type: "for_loop";
|
||||
loop_over: WorkflowParameter;
|
||||
loop_blocks: Array<WorkflowBlock>;
|
||||
};
|
||||
|
||||
export type CodeBlock = WorkflowBlockBase & {
|
||||
block_type: "code";
|
||||
code: string;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
};
|
||||
|
||||
export type TextPromptBlock = WorkflowBlockBase & {
|
||||
block_type: "text_prompt";
|
||||
llm_key: string;
|
||||
prompt: string;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
json_schema: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type DownloadToS3Block = WorkflowBlockBase & {
|
||||
block_type: "download_to_s3";
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type UploadToS3Block = WorkflowBlockBase & {
|
||||
block_type: "upload_to_s3";
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type SendEmailBlock = WorkflowBlockBase & {
|
||||
block_type: "send_email";
|
||||
smtp_host: AWSSecretParameter;
|
||||
smtp_port: AWSSecretParameter;
|
||||
smtp_username: AWSSecretParameter;
|
||||
smtp_password: AWSSecretParameter;
|
||||
sender: string;
|
||||
recipients: Array<string>;
|
||||
subject: string;
|
||||
body: string;
|
||||
file_attachments: Array<string>;
|
||||
};
|
||||
|
||||
export type FileURLParserBlock = WorkflowBlockBase & {
|
||||
block_type: "file_url_parser";
|
||||
file_url: string;
|
||||
file_type: "csv";
|
||||
};
|
||||
|
||||
export type WorkflowBlock =
|
||||
| TaskBlock
|
||||
| ForLoopBlock
|
||||
| TextPromptBlock
|
||||
| CodeBlock
|
||||
| UploadToS3Block
|
||||
| DownloadToS3Block
|
||||
| SendEmailBlock
|
||||
| FileURLParserBlock;
|
||||
|
||||
export type WorkflowDefinition = {
|
||||
parameters: Array<WorkflowParameter>;
|
||||
blocks: Array<WorkflowBlock>;
|
||||
};
|
||||
|
||||
export type WorkflowApiResponse = {
|
||||
workflow_id: string;
|
||||
organization_id: string;
|
||||
is_saved_task: boolean;
|
||||
title: string;
|
||||
workflow_permanent_id: string;
|
||||
version: number;
|
||||
description: string;
|
||||
workflow_definition: WorkflowDefinition;
|
||||
proxy_location: string;
|
||||
webhook_callback_url: string;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
};
|
||||
@@ -19,7 +19,11 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
slate: {
|
||||
elevation1: "hsl(var(--slate-elevation-1))",
|
||||
elevation2: "hsl(var(--slate-elevation-2))",
|
||||
elevation3: "hsl(var(--slate-elevation-3))",
|
||||
elevation4: "hsl(var(--slate-elevation-4))",
|
||||
elevation5: "hsl(var(--slate-elevation-5))",
|
||||
},
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
|
||||
Reference in New Issue
Block a user