Editable workflows (#792)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
414
skyvern-frontend/package-lock.json
generated
414
skyvern-frontend/package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
@@ -1622,6 +1623,419 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==",
|
||||||
|
"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-dismissable-layer": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.0",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-popper": "1.2.0",
|
||||||
|
"@radix-ui/react-portal": "1.1.1",
|
||||||
|
"@radix-ui/react-presence": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-slot": "1.1.0",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||||
|
"aria-hidden": "^1.1.1",
|
||||||
|
"react-remove-scroll": "2.5.7"
|
||||||
|
},
|
||||||
|
"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-popover/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-popover/node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.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-popover/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-popover/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-popover/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "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-popover/node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==",
|
||||||
|
"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-popover/node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "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-popover/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
|
||||||
|
"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-popover/node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "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-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.0",
|
||||||
|
"@radix-ui/react-use-size": "1.1.0",
|
||||||
|
"@radix-ui/rect": "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-popover/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "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-popover/node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "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-popover/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-popover/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-popover/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-popover/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-popover/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||||
|
"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-popover/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-popover/node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "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-popover/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-popover/node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz",
|
||||||
|
"integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.4",
|
||||||
|
"react-style-singleton": "^2.2.1",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.0",
|
||||||
|
"use-sidecar": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
|||||||
24
skyvern-frontend/src/components/icons/GarbageIcon.tsx
Normal file
24
skyvern-frontend/src/components/icons/GarbageIcon.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
type Props = {
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function GarbageIcon({ className }: Props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { GarbageIcon };
|
||||||
21
skyvern-frontend/src/components/icons/SaveIcon.tsx
Normal file
21
skyvern-frontend/src/components/icons/SaveIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
function SaveIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7 3V6.4C7 6.96005 7 7.24008 7.10899 7.45399C7.20487 7.64215 7.35785 7.79513 7.54601 7.89101C7.75992 8 8.03995 8 8.6 8H15.4C15.9601 8 16.2401 8 16.454 7.89101C16.6422 7.79513 16.7951 7.64215 16.891 7.45399C17 7.24008 17 6.96005 17 6.4V4M17 21V14.6C17 14.0399 17 13.7599 16.891 13.546C16.7951 13.3578 16.6422 13.2049 16.454 13.109C16.2401 13 15.9601 13 15.4 13H8.6C8.03995 13 7.75992 13 7.54601 13.109C7.35785 13.2049 7.20487 13.3578 7.10899 13.546C7 13.7599 7 14.0399 7 14.6V21M21 9.32548V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V7.8C3 6.11984 3 5.27976 3.32698 4.63803C3.6146 4.07354 4.07354 3.6146 4.63803 3.32698C5.27976 3 6.11984 3 7.8 3H14.6745C15.1637 3 15.4083 3 15.6385 3.05526C15.8425 3.10425 16.0376 3.18506 16.2166 3.29472C16.4184 3.4184 16.5914 3.59135 16.9373 3.93726L20.0627 7.06274C20.4086 7.40865 20.5816 7.5816 20.7053 7.78343C20.8149 7.96237 20.8957 8.15746 20.9447 8.36154C21 8.59171 21 8.8363 21 9.32548Z"
|
||||||
|
stroke="#F8FAFC"
|
||||||
|
strokeWidth="1.6"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { SaveIcon };
|
||||||
31
skyvern-frontend/src/components/ui/popover.tsx
Normal file
31
skyvern-frontend/src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||||
|
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
|
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
));
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
@@ -31,15 +31,29 @@ import {
|
|||||||
CounterClockwiseClockIcon,
|
CounterClockwiseClockIcon,
|
||||||
Pencil2Icon,
|
Pencil2Icon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ReloadIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard";
|
import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard";
|
||||||
import { WorkflowTitle } from "./WorkflowTitle";
|
import { WorkflowTitle } from "./WorkflowTitle";
|
||||||
|
import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes";
|
||||||
|
import { stringify as convertToYAML } from "yaml";
|
||||||
|
|
||||||
|
const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
||||||
|
title: "New Workflow",
|
||||||
|
description: "",
|
||||||
|
workflow_definition: {
|
||||||
|
blocks: [],
|
||||||
|
parameters: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function Workflows() {
|
function Workflows() {
|
||||||
const credentialGetter = useCredentialGetter();
|
const credentialGetter = useCredentialGetter();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const workflowsPage = searchParams.get("workflowsPage")
|
const workflowsPage = searchParams.get("workflowsPage")
|
||||||
? Number(searchParams.get("workflowsPage"))
|
? Number(searchParams.get("workflowsPage"))
|
||||||
@@ -79,6 +93,27 @@ function Workflows() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createNewWorkflowMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
const yaml = convertToYAML(emptyWorkflowRequest);
|
||||||
|
return client.post<
|
||||||
|
typeof emptyWorkflowRequest,
|
||||||
|
{ data: WorkflowApiResponse }
|
||||||
|
>("/workflows", yaml, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (response) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflows"],
|
||||||
|
});
|
||||||
|
navigate(`/workflows/${response.data.workflow_permanent_id}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (workflows?.length === 0 && workflowsPage === 1) {
|
if (workflows?.length === 0 && workflowsPage === 1) {
|
||||||
return <WorkflowsBetaAlertCard />;
|
return <WorkflowsBetaAlertCard />;
|
||||||
}
|
}
|
||||||
@@ -115,8 +150,21 @@ function Workflows() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<header>
|
<header className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl font-semibold">Workflows</h1>
|
<h1 className="text-2xl font-semibold">Workflows</h1>
|
||||||
|
<Button
|
||||||
|
disabled={createNewWorkflowMutation.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
createNewWorkflowMutation.mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{createNewWorkflowMutation.isPending ? (
|
||||||
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Create Workflow
|
||||||
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import CodeMirror from "@uiw/react-codemirror";
|
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { python } from "@codemirror/lang-python";
|
import { python } from "@codemirror/lang-python";
|
||||||
import { tokyoNightStorm } from "@uiw/codemirror-theme-tokyo-night-storm";
|
import { tokyoNightStorm } from "@uiw/codemirror-theme-tokyo-night-storm";
|
||||||
@@ -22,7 +22,10 @@ function CodeEditor({
|
|||||||
className,
|
className,
|
||||||
fontSize = 8,
|
fontSize = 8,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const extensions = language === "json" ? [json()] : [python()];
|
const extensions =
|
||||||
|
language === "json"
|
||||||
|
? [json(), EditorView.lineWrapping]
|
||||||
|
: [python(), EditorView.lineWrapping];
|
||||||
return (
|
return (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -13,23 +13,100 @@ import "@xyflow/react/dist/style.css";
|
|||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
import { AppNode, nodeTypes } from "./nodes";
|
import { AppNode, nodeTypes } from "./nodes";
|
||||||
import "./reactFlowOverrideStyles.css";
|
import "./reactFlowOverrideStyles.css";
|
||||||
import { layout } from "./workflowEditorUtils";
|
import { createNode, getWorkflowBlocks, layout } from "./workflowEditorUtils";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||||
|
import { edgeTypes } from "./edges";
|
||||||
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
|
import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
||||||
|
import {
|
||||||
|
BitwardenLoginCredentialParameterYAML,
|
||||||
|
BlockYAML,
|
||||||
|
WorkflowParameterYAML,
|
||||||
|
} from "../types/workflowYamlTypes";
|
||||||
|
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||||
|
import { WorkflowParameterValueType } from "../types/workflowTypes";
|
||||||
|
|
||||||
|
function convertToParametersYAML(
|
||||||
|
parameters: ParametersState,
|
||||||
|
): Array<WorkflowParameterYAML | BitwardenLoginCredentialParameterYAML> {
|
||||||
|
return parameters.map((parameter) => {
|
||||||
|
if (parameter.parameterType === "workflow") {
|
||||||
|
return {
|
||||||
|
parameter_type: "workflow",
|
||||||
|
key: parameter.key,
|
||||||
|
description: parameter.description || null,
|
||||||
|
workflow_parameter_type: parameter.dataType,
|
||||||
|
default_value: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
parameter_type: "bitwarden_login_credential",
|
||||||
|
key: parameter.key,
|
||||||
|
description: parameter.description || null,
|
||||||
|
bitwarden_collection_id: parameter.collectionId,
|
||||||
|
url_parameter_key: parameter.urlParameterKey,
|
||||||
|
bitwarden_client_id_aws_secret_key: "SKYVERN_BITWARDEN_CLIENT_ID",
|
||||||
|
bitwarden_client_secret_aws_secret_key:
|
||||||
|
"SKYVERN_BITWARDEN_CLIENT_SECRET",
|
||||||
|
bitwarden_master_password_aws_secret_key:
|
||||||
|
"SKYVERN_BITWARDEN_MASTER_PASSWORD",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParametersState = Array<
|
||||||
|
| {
|
||||||
|
key: string;
|
||||||
|
parameterType: "workflow";
|
||||||
|
dataType: WorkflowParameterValueType;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
key: string;
|
||||||
|
parameterType: "credential";
|
||||||
|
collectionId: string;
|
||||||
|
urlParameterKey: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
initialTitle: string;
|
||||||
initialNodes: Array<AppNode>;
|
initialNodes: Array<AppNode>;
|
||||||
initialEdges: Array<Edge>;
|
initialEdges: Array<Edge>;
|
||||||
|
initialParameters: ParametersState;
|
||||||
|
handleSave: (
|
||||||
|
parameters: Array<
|
||||||
|
WorkflowParameterYAML | BitwardenLoginCredentialParameterYAML
|
||||||
|
>,
|
||||||
|
blocks: Array<BlockYAML>,
|
||||||
|
title: string,
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function FlowRenderer({ title, initialEdges, initialNodes }: Props) {
|
export type AddNodeProps = {
|
||||||
const [rightSidePanelOpen, setRightSidePanelOpen] = useState(false);
|
nodeType: Exclude<keyof typeof nodeTypes, "nodeAdder">;
|
||||||
const [rightSidePanelContent, setRightSidePanelContent] = useState<
|
previous: string | null;
|
||||||
"parameters" | "nodeLibrary" | null
|
next: string | null;
|
||||||
>(null);
|
parent?: string;
|
||||||
|
connectingEdgeType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function FlowRenderer({
|
||||||
|
initialTitle,
|
||||||
|
initialEdges,
|
||||||
|
initialNodes,
|
||||||
|
initialParameters,
|
||||||
|
handleSave,
|
||||||
|
}: Props) {
|
||||||
|
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
||||||
|
useWorkflowPanelStore();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const [parameters, setParameters] = useState(initialParameters);
|
||||||
|
const [title, setTitle] = useState(initialTitle);
|
||||||
const nodesInitialized = useNodesInitialized();
|
const nodesInitialized = useNodesInitialized();
|
||||||
|
|
||||||
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
||||||
@@ -45,62 +122,173 @@ function FlowRenderer({ title, initialEdges, initialNodes }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [nodesInitialized]);
|
}, [nodesInitialized]);
|
||||||
|
|
||||||
|
function addNode({
|
||||||
|
nodeType,
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
parent,
|
||||||
|
connectingEdgeType,
|
||||||
|
}: AddNodeProps) {
|
||||||
|
const newNodes: Array<AppNode> = [];
|
||||||
|
const newEdges: Array<Edge> = [];
|
||||||
|
const index = parent
|
||||||
|
? nodes.filter((node) => node.parentId === parent).length
|
||||||
|
: nodes.length;
|
||||||
|
const id = parent ? `${parent}-${index}` : String(index);
|
||||||
|
const node = createNode({ id, parentId: parent }, nodeType, String(index));
|
||||||
|
newNodes.push(node);
|
||||||
|
if (previous) {
|
||||||
|
const newEdge = {
|
||||||
|
id: `edge-${previous}-${id}`,
|
||||||
|
type: "edgeWithAddButton",
|
||||||
|
source: previous,
|
||||||
|
target: id,
|
||||||
|
style: {
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newEdges.push(newEdge);
|
||||||
|
}
|
||||||
|
if (next) {
|
||||||
|
const newEdge = {
|
||||||
|
id: `edge-${id}-${next}`,
|
||||||
|
type: connectingEdgeType,
|
||||||
|
source: id,
|
||||||
|
target: next,
|
||||||
|
style: {
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
newEdges.push(newEdge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeType === "loop") {
|
||||||
|
newNodes.push({
|
||||||
|
id: `${id}-nodeAdder`,
|
||||||
|
type: "nodeAdder",
|
||||||
|
parentId: id,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {},
|
||||||
|
draggable: false,
|
||||||
|
connectable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const editedEdges = previous
|
||||||
|
? edges.filter((edge) => edge.source !== previous)
|
||||||
|
: edges;
|
||||||
|
|
||||||
|
const previousNode = nodes.find((node) => node.id === previous);
|
||||||
|
const previousNodeIndex = previousNode
|
||||||
|
? nodes.indexOf(previousNode)
|
||||||
|
: nodes.length - 1;
|
||||||
|
|
||||||
|
const newNodesAfter = [
|
||||||
|
...nodes.slice(0, previousNodeIndex + 1),
|
||||||
|
...newNodes,
|
||||||
|
...nodes.slice(previousNodeIndex + 1),
|
||||||
|
];
|
||||||
|
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlow
|
<WorkflowParametersStateContext.Provider
|
||||||
nodes={nodes}
|
value={[parameters, setParameters]}
|
||||||
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" />
|
<ReactFlow
|
||||||
<Controls position="bottom-left" />
|
nodes={nodes}
|
||||||
<Panel position="top-center" className="h-20">
|
edges={edges}
|
||||||
<WorkflowHeader
|
onNodesChange={(changes) => {
|
||||||
title={title}
|
const dimensionChanges = changes.filter(
|
||||||
parametersPanelOpen={rightSidePanelOpen}
|
(change) => change.type === "dimensions",
|
||||||
onParametersClick={() => {
|
);
|
||||||
setRightSidePanelOpen((open) => !open);
|
const tempNodes = [...nodes];
|
||||||
setRightSidePanelContent("parameters");
|
dimensionChanges.forEach((change) => {
|
||||||
}}
|
const node = tempNodes.find((node) => node.id === change.id);
|
||||||
/>
|
if (node) {
|
||||||
</Panel>
|
if (node.measured?.width) {
|
||||||
{rightSidePanelOpen && (
|
node.measured.width = change.dimensions?.width;
|
||||||
<Panel
|
}
|
||||||
position="top-right"
|
if (node.measured?.height) {
|
||||||
className="w-96 rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"
|
node.measured.height = change.dimensions?.height;
|
||||||
>
|
}
|
||||||
{rightSidePanelContent === "parameters" && (
|
}
|
||||||
<WorkflowParametersPanel />
|
});
|
||||||
)}
|
if (dimensionChanges.length > 0) {
|
||||||
|
doLayout(tempNodes, edges);
|
||||||
|
}
|
||||||
|
onNodesChange(changes);
|
||||||
|
}}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
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}
|
||||||
|
onTitleChange={setTitle}
|
||||||
|
parametersPanelOpen={
|
||||||
|
workflowPanelState.active &&
|
||||||
|
workflowPanelState.content === "parameters"
|
||||||
|
}
|
||||||
|
onParametersClick={() => {
|
||||||
|
if (
|
||||||
|
workflowPanelState.active &&
|
||||||
|
workflowPanelState.content === "parameters"
|
||||||
|
) {
|
||||||
|
closeWorkflowPanel();
|
||||||
|
} else {
|
||||||
|
setWorkflowPanelState({
|
||||||
|
active: true,
|
||||||
|
content: "parameters",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSave={() => {
|
||||||
|
const blocksInYAMLConvertibleJSON = getWorkflowBlocks(nodes);
|
||||||
|
const parametersInYAMLConvertibleJSON =
|
||||||
|
convertToParametersYAML(parameters);
|
||||||
|
handleSave(
|
||||||
|
parametersInYAMLConvertibleJSON,
|
||||||
|
blocksInYAMLConvertibleJSON,
|
||||||
|
title,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
)}
|
{workflowPanelState.active && (
|
||||||
</ReactFlow>
|
<Panel position="top-right">
|
||||||
|
{workflowPanelState.content === "parameters" && (
|
||||||
|
<WorkflowParametersPanel />
|
||||||
|
)}
|
||||||
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
|
<WorkflowNodeLibraryPanel
|
||||||
|
onNodeClick={(props) => {
|
||||||
|
addNode(props);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
{nodes.length === 0 && (
|
||||||
|
<Panel position="top-right">
|
||||||
|
<WorkflowNodeLibraryPanel
|
||||||
|
onNodeClick={(props) => {
|
||||||
|
addNode(props);
|
||||||
|
}}
|
||||||
|
first
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
</ReactFlow>
|
||||||
|
</WorkflowParametersStateContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,75 @@
|
|||||||
import { ReactFlowProvider } from "@xyflow/react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||||
import { FlowRenderer } from "./FlowRenderer";
|
|
||||||
import { getElements } from "./workflowEditorUtils";
|
import { getElements } from "./workflowEditorUtils";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
BlockYAML,
|
||||||
|
ParameterYAML,
|
||||||
|
WorkflowCreateYAMLRequest,
|
||||||
|
} from "../types/workflowYamlTypes";
|
||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { stringify as convertToYAML } from "yaml";
|
||||||
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
|
import { FlowRenderer } from "./FlowRenderer";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
function WorkflowEditor() {
|
function WorkflowEditor() {
|
||||||
const { workflowPermanentId } = useParams();
|
const { workflowPermanentId } = useParams();
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
|
||||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const saveWorkflowMutation = useMutation({
|
||||||
|
mutationFn: async (data: {
|
||||||
|
parameters: Array<ParameterYAML>;
|
||||||
|
blocks: Array<BlockYAML>;
|
||||||
|
title: string;
|
||||||
|
}) => {
|
||||||
|
if (!workflow || !workflowPermanentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
const requestBody: WorkflowCreateYAMLRequest = {
|
||||||
|
title: data.title,
|
||||||
|
description: workflow.description,
|
||||||
|
proxy_location: workflow.proxy_location,
|
||||||
|
webhook_callback_url: workflow.webhook_callback_url,
|
||||||
|
totp_verification_url: workflow.totp_verification_url,
|
||||||
|
workflow_definition: {
|
||||||
|
parameters: data.parameters,
|
||||||
|
blocks: data.blocks,
|
||||||
|
},
|
||||||
|
is_saved_task: workflow.is_saved_task,
|
||||||
|
};
|
||||||
|
const yaml = convertToYAML(requestBody);
|
||||||
|
return client
|
||||||
|
.put(`/workflows/${workflowPermanentId}`, yaml, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.data);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Changes saved",
|
||||||
|
description: "Your changes have been saved",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -30,9 +89,38 @@ function WorkflowEditor() {
|
|||||||
<div className="h-screen w-full">
|
<div className="h-screen w-full">
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<FlowRenderer
|
<FlowRenderer
|
||||||
title={workflow.title}
|
initialTitle={workflow.title}
|
||||||
initialNodes={elements.nodes}
|
initialNodes={elements.nodes}
|
||||||
initialEdges={elements.edges}
|
initialEdges={elements.edges}
|
||||||
|
initialParameters={workflow.workflow_definition.parameters
|
||||||
|
.filter(
|
||||||
|
(parameter) =>
|
||||||
|
parameter.parameter_type === "workflow" ||
|
||||||
|
parameter.parameter_type === "bitwarden_login_credential",
|
||||||
|
)
|
||||||
|
.map((parameter) => {
|
||||||
|
if (parameter.parameter_type === "workflow") {
|
||||||
|
return {
|
||||||
|
key: parameter.key,
|
||||||
|
parameterType: "workflow",
|
||||||
|
dataType: parameter.workflow_parameter_type,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
key: parameter.key,
|
||||||
|
parameterType: "credential",
|
||||||
|
collectionId: parameter.bitwarden_collection_id,
|
||||||
|
urlParameterKey: parameter.url_parameter_key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
handleSave={(parameters, blocks, title) => {
|
||||||
|
saveWorkflowMutation.mutate({
|
||||||
|
parameters,
|
||||||
|
blocks,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { SaveIcon } from "@/components/icons/SaveIcon";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
@@ -6,17 +7,22 @@ import {
|
|||||||
PlayIcon,
|
PlayIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { EditableNodeTitle } from "./nodes/components/EditableNodeTitle";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
parametersPanelOpen: boolean;
|
parametersPanelOpen: boolean;
|
||||||
onParametersClick: () => void;
|
onParametersClick: () => void;
|
||||||
|
onSave: () => void;
|
||||||
|
onTitleChange: (title: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function WorkflowHeader({
|
function WorkflowHeader({
|
||||||
title,
|
title,
|
||||||
parametersPanelOpen,
|
parametersPanelOpen,
|
||||||
onParametersClick,
|
onParametersClick,
|
||||||
|
onSave,
|
||||||
|
onTitleChange,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { workflowPermanentId } = useParams();
|
const { workflowPermanentId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -24,17 +30,34 @@ function WorkflowHeader({
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full bg-slate-elevation2">
|
<div className="flex h-full w-full bg-slate-elevation2">
|
||||||
<div className="flex h-full w-1/3 items-center pl-6">
|
<div className="flex h-full w-1/3 items-center pl-6">
|
||||||
<div
|
<div className="flex">
|
||||||
className="cursor-pointer rounded-full p-2 hover:bg-slate-elevation5"
|
<div
|
||||||
onClick={() => {
|
className="cursor-pointer rounded-full p-2 hover:bg-slate-elevation5"
|
||||||
navigate("/workflows");
|
onClick={() => {
|
||||||
}}
|
navigate("/workflows");
|
||||||
>
|
}}
|
||||||
<ExitIcon className="h-6 w-6" />
|
>
|
||||||
|
<ExitIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer rounded-full p-2 hover:bg-slate-elevation5"
|
||||||
|
onClick={() => {
|
||||||
|
onSave();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SaveIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-1/3 items-center justify-center">
|
<div className="flex h-full w-1/3 items-center justify-center p-1">
|
||||||
<span className="max-w-max truncate text-3xl">{title}</span>
|
<EditableNodeTitle
|
||||||
|
editable={true}
|
||||||
|
onChange={onTitleChange}
|
||||||
|
value={title}
|
||||||
|
className="max-w-96 text-3xl"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-1/3 items-center justify-end gap-4 p-4">
|
<div className="flex h-full w-1/3 items-center justify-end gap-4 p-4">
|
||||||
<Button variant="secondary" size="lg" onClick={onParametersClick}>
|
<Button variant="secondary" size="lg" onClick={onParametersClick}>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import { ParametersState } from "./FlowRenderer";
|
||||||
|
|
||||||
|
type WorkflowParametersState = [
|
||||||
|
ParametersState,
|
||||||
|
React.Dispatch<React.SetStateAction<ParametersState>>,
|
||||||
|
];
|
||||||
|
|
||||||
|
const WorkflowParametersStateContext = createContext<
|
||||||
|
WorkflowParametersState | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export { WorkflowParametersStateContext };
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// nodes have 1000 Z index and we want edges above
|
||||||
|
export const REACT_FLOW_EDGE_Z_INDEX = 1001;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
|
import { PlusIcon } from "@radix-ui/react-icons";
|
||||||
|
import {
|
||||||
|
BaseEdge,
|
||||||
|
EdgeLabelRenderer,
|
||||||
|
EdgeProps,
|
||||||
|
getBezierPath,
|
||||||
|
useNodes,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import { REACT_FLOW_EDGE_Z_INDEX } from "../constants";
|
||||||
|
|
||||||
|
function EdgeWithAddButton({
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
style = {},
|
||||||
|
markerEnd,
|
||||||
|
}: EdgeProps) {
|
||||||
|
const nodes = useNodes();
|
||||||
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
});
|
||||||
|
const setWorkflowPanelState = useWorkflowPanelStore(
|
||||||
|
(state) => state.setWorkflowPanelState,
|
||||||
|
);
|
||||||
|
const sourceNode = nodes.find((node) => node.id === source);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
fontSize: 12,
|
||||||
|
// everything inside EdgeLabelRenderer has no pointer events by default
|
||||||
|
// if you have an interactive element, set pointer-events: all
|
||||||
|
pointerEvents: "all",
|
||||||
|
zIndex: REACT_FLOW_EDGE_Z_INDEX + 1, // above the edge
|
||||||
|
}}
|
||||||
|
className="nodrag nopan"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="h-4 w-4 rounded-full transition-all hover:scale-150"
|
||||||
|
onClick={() => {
|
||||||
|
setWorkflowPanelState({
|
||||||
|
active: true,
|
||||||
|
content: "nodeLibrary",
|
||||||
|
data: {
|
||||||
|
previous: source,
|
||||||
|
next: target,
|
||||||
|
parent: sourceNode?.parentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { EdgeWithAddButton };
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { EdgeWithAddButton } from "./EdgeWithAddButton";
|
||||||
|
|
||||||
|
export const edgeTypes = {
|
||||||
|
edgeWithAddButton: EdgeWithAddButton,
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
import type { CodeBlockNode } from "./types";
|
import type { CodeBlockNode } from "./types";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { CodeIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
import { CodeIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
|
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
|
||||||
function CodeBlockNode({ data }: NodeProps<CodeBlockNode>) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -26,8 +29,12 @@ function CodeBlockNode({ data }: NodeProps<CodeBlockNode>) {
|
|||||||
<CodeIcon className="h-6 w-6" />
|
<CodeIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
<span className="text-xs text-slate-400">Task Block</span>
|
value={data.label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">Code Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -39,9 +46,11 @@ function CodeBlockNode({ data }: NodeProps<CodeBlockNode>) {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="python"
|
language="python"
|
||||||
value={data.code}
|
value={data.code}
|
||||||
onChange={() => {
|
onChange={(value) => {
|
||||||
if (!data.editable) return;
|
if (!data.editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { code: value });
|
||||||
}}
|
}}
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export type CodeBlockNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CodeBlockNode = Node<CodeBlockNodeData, "codeBlock">;
|
export type CodeBlockNode = Node<CodeBlockNodeData, "codeBlock">;
|
||||||
|
|
||||||
|
export const codeBlockNodeDefaultData: CodeBlockNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
code: "",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { DotsHorizontalIcon, DownloadIcon } from "@radix-ui/react-icons";
|
import { DotsHorizontalIcon, DownloadIcon } from "@radix-ui/react-icons";
|
||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
import type { DownloadNode } from "./types";
|
import type { DownloadNode } from "./types";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
|
function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
|
||||||
function DownloadNode({ data }: NodeProps<DownloadNode>) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -26,7 +29,11 @@ function DownloadNode({ data }: NodeProps<DownloadNode>) {
|
|||||||
<DownloadIcon className="h-6 w-6" />
|
<DownloadIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">Download Block</span>
|
<span className="text-xs text-slate-400">Download Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,11 +46,11 @@ function DownloadNode({ data }: NodeProps<DownloadNode>) {
|
|||||||
<Label className="text-sm text-slate-400">File URL</Label>
|
<Label className="text-sm text-slate-400">File URL</Label>
|
||||||
<Input
|
<Input
|
||||||
value={data.url}
|
value={data.url}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) {
|
if (!data.editable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO
|
updateNodeData(id, { url: event.target.value });
|
||||||
}}
|
}}
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export type DownloadNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DownloadNode = Node<DownloadNodeData, "download">;
|
export type DownloadNode = Node<DownloadNodeData, "download">;
|
||||||
|
|
||||||
|
export const downloadNodeDefaultData: DownloadNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
url: "SKYVERN_DOWNLOAD_DIRECTORY",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
import type { FileParserNode } from "./types";
|
import type { FileParserNode } from "./types";
|
||||||
import { CursorTextIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
import { CursorTextIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
function FileParserNode({ data }: NodeProps<FileParserNode>) {
|
function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -25,7 +27,11 @@ function FileParserNode({ data }: NodeProps<FileParserNode>) {
|
|||||||
<CursorTextIcon className="h-6 w-6" />
|
<CursorTextIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">File Parser Block</span>
|
<span className="text-xs text-slate-400">File Parser Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,11 +44,11 @@ function FileParserNode({ data }: NodeProps<FileParserNode>) {
|
|||||||
<span className="text-sm text-slate-400">File URL</span>
|
<span className="text-sm text-slate-400">File URL</span>
|
||||||
<Input
|
<Input
|
||||||
value={data.fileUrl}
|
value={data.fileUrl}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) {
|
if (!data.editable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO
|
updateNodeData(id, { fileUrl: event.target.value });
|
||||||
}}
|
}}
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export type FileParserNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type FileParserNode = Node<FileParserNodeData, "fileParser">;
|
export type FileParserNode = Node<FileParserNodeData, "fileParser">;
|
||||||
|
|
||||||
|
export const fileParserNodeDefaultData: FileParserNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
fileUrl: "",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { DotsHorizontalIcon, UpdateIcon } from "@radix-ui/react-icons";
|
import { DotsHorizontalIcon, UpdateIcon } from "@radix-ui/react-icons";
|
||||||
import { Handle, NodeProps, Position, useNodes } from "@xyflow/react";
|
import {
|
||||||
|
Handle,
|
||||||
|
NodeProps,
|
||||||
|
Position,
|
||||||
|
useNodes,
|
||||||
|
useReactFlow,
|
||||||
|
} from "@xyflow/react";
|
||||||
import type { LoopNode } from "./types";
|
import type { LoopNode } from "./types";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import type { Node } from "@xyflow/react";
|
import type { Node } from "@xyflow/react";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
const nodes = useNodes();
|
const nodes = useNodes();
|
||||||
const children = nodes.filter((node) => node.parentId === id);
|
const children = nodes.filter((node) => node.parentId === id);
|
||||||
const furthestDownChild: Node | null = children.reduce(
|
const furthestDownChild: Node | null = children.reduce(
|
||||||
@@ -54,7 +62,11 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
|||||||
<UpdateIcon className="h-6 w-6" />
|
<UpdateIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">Loop Block</span>
|
<span className="text-xs text-slate-400">Loop Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,11 +78,11 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
|||||||
<Label className="text-xs text-slate-300">Loop Value</Label>
|
<Label className="text-xs text-slate-300">Loop Value</Label>
|
||||||
<Input
|
<Input
|
||||||
value={data.loopValue}
|
value={data.loopValue}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) {
|
if (!data.editable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO
|
updateNodeData(id, { loopValue: event.target.value });
|
||||||
}}
|
}}
|
||||||
placeholder="What value are you iterating over?"
|
placeholder="What value are you iterating over?"
|
||||||
className="nopan"
|
className="nopan"
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export type LoopNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type LoopNode = Node<LoopNodeData, "loop">;
|
export type LoopNode = Node<LoopNodeData, "loop">;
|
||||||
|
|
||||||
|
export const loopNodeDefaultData: LoopNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
loopValue: "",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Handle, NodeProps, Position, useEdges } from "@xyflow/react";
|
||||||
|
import type { NodeAdderNode } from "./types";
|
||||||
|
import { PlusIcon } from "@radix-ui/react-icons";
|
||||||
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
|
|
||||||
|
function NodeAdderNode({ id, parentId }: NodeProps<NodeAdderNode>) {
|
||||||
|
const edges = useEdges();
|
||||||
|
const setWorkflowPanelState = useWorkflowPanelStore(
|
||||||
|
(state) => state.setWorkflowPanelState,
|
||||||
|
);
|
||||||
|
|
||||||
|
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="rounded-full bg-slate-50 p-2"
|
||||||
|
onClick={() => {
|
||||||
|
const previous = edges.find((edge) => edge.target === id)?.source;
|
||||||
|
setWorkflowPanelState({
|
||||||
|
active: true,
|
||||||
|
content: "nodeLibrary",
|
||||||
|
data: {
|
||||||
|
previous: previous ?? null,
|
||||||
|
next: id,
|
||||||
|
parent: parentId,
|
||||||
|
connectingEdgeType: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-12 w-12 text-slate-950" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NodeAdderNode };
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { Node } from "@xyflow/react";
|
||||||
|
|
||||||
|
export type NodeAdderNodeData = Record<string, never>;
|
||||||
|
|
||||||
|
export type NodeAdderNode = Node<NodeAdderNodeData, "nodeAdder">;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
import type { SendEmailNode } from "./types";
|
import type { SendEmailNode } from "./types";
|
||||||
import { DotsHorizontalIcon, EnvelopeClosedIcon } from "@radix-ui/react-icons";
|
import { DotsHorizontalIcon, EnvelopeClosedIcon } from "@radix-ui/react-icons";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
|
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
|
||||||
function SendEmailNode({ data }: NodeProps<SendEmailNode>) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -28,7 +30,11 @@ function SendEmailNode({ data }: NodeProps<SendEmailNode>) {
|
|||||||
<EnvelopeClosedIcon className="h-6 w-6" />
|
<EnvelopeClosedIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">Send Email Block</span>
|
<span className="text-xs text-slate-400">Send Email Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,24 +43,42 @@ function SendEmailNode({ data }: NodeProps<SendEmailNode>) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">Recipient</Label>
|
<Label className="text-xs text-slate-300">Sender</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) return;
|
if (!data.editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { sender: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.recipients.join(", ")}
|
value={data.sender}
|
||||||
placeholder="example@gmail.com"
|
placeholder="example@gmail.com"
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Recipients</Label>
|
||||||
|
<Input
|
||||||
|
onChange={(event) => {
|
||||||
|
if (!data.editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { recipients: event.target.value });
|
||||||
|
}}
|
||||||
|
value={data.recipients}
|
||||||
|
placeholder="example@gmail.com, example2@gmail.com..."
|
||||||
|
className="nopan"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">Subject</Label>
|
<Label className="text-xs text-slate-300">Subject</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) return;
|
if (!data.editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { subject: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.subject}
|
value={data.subject}
|
||||||
placeholder="What is the gist?"
|
placeholder="What is the gist?"
|
||||||
@@ -64,9 +88,11 @@ function SendEmailNode({ data }: NodeProps<SendEmailNode>) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">Body</Label>
|
<Label className="text-xs text-slate-300">Body</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) return;
|
if (!data.editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { body: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.body}
|
value={data.body}
|
||||||
placeholder="What would you like to say?"
|
placeholder="What would you like to say?"
|
||||||
@@ -77,21 +103,16 @@ function SendEmailNode({ data }: NodeProps<SendEmailNode>) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">File Attachments</Label>
|
<Label className="text-xs text-slate-300">File Attachments</Label>
|
||||||
<Input
|
<Input
|
||||||
value={data.fileAttachments?.join(", ") ?? ""}
|
value={data.fileAttachments}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) return;
|
if (!data.editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { fileAttachments: event.target.value });
|
||||||
}}
|
}}
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import type { Node } from "@xyflow/react";
|
import type { Node } from "@xyflow/react";
|
||||||
|
|
||||||
export type SendEmailNodeData = {
|
export type SendEmailNodeData = {
|
||||||
recipients: string[];
|
recipients: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
body: string;
|
body: string;
|
||||||
fileAttachments: string[] | null;
|
fileAttachments: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
|
sender: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SendEmailNode = Node<SendEmailNodeData, "sendEmail">;
|
export type SendEmailNode = Node<SendEmailNodeData, "sendEmail">;
|
||||||
|
|
||||||
|
export const sendEmailNodeDefaultData: SendEmailNodeData = {
|
||||||
|
recipients: "",
|
||||||
|
subject: "",
|
||||||
|
body: "",
|
||||||
|
fileAttachments: "",
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
sender: "",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
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 { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
AccordionContent,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion";
|
} from "@/components/ui/accordion";
|
||||||
import { DataSchema } from "../../../components/DataSchema";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { TaskNodeErrorMapping } from "./TaskNodeErrorMapping";
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import {
|
||||||
|
DotsHorizontalIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
MixerVerticalIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch";
|
||||||
|
import { TaskNodeParametersPanel } from "./TaskNodeParametersPanel";
|
||||||
|
import type { TaskNode, TaskNodeDisplayMode } from "./types";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
function TaskNode({ data }: NodeProps<TaskNode>) {
|
function TaskNode({ id, data }: NodeProps<TaskNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>("basic");
|
const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>("basic");
|
||||||
const { editable } = data;
|
const { editable } = data;
|
||||||
|
|
||||||
@@ -28,9 +40,12 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
value={data.url}
|
value={data.url}
|
||||||
className="nopan"
|
className="nopan"
|
||||||
onChange={() => {
|
name="url"
|
||||||
if (!editable) return;
|
onChange={(event) => {
|
||||||
// TODO
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { url: event.target.value });
|
||||||
}}
|
}}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
/>
|
/>
|
||||||
@@ -38,9 +53,11 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">Goal</Label>
|
<Label className="text-xs text-slate-300">Goal</Label>
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { navigationGoal: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.navigationGoal}
|
value={data.navigationGoal}
|
||||||
placeholder="What are you looking to do?"
|
placeholder="What are you looking to do?"
|
||||||
@@ -63,9 +80,11 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">URL</Label>
|
<Label className="text-xs text-slate-300">URL</Label>
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { url: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.url}
|
value={data.url}
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
@@ -75,9 +94,11 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">Goal</Label>
|
<Label className="text-xs text-slate-300">Goal</Label>
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { navigationGoal: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.navigationGoal}
|
value={data.navigationGoal}
|
||||||
placeholder="What are you looking to do?"
|
placeholder="What are you looking to do?"
|
||||||
@@ -96,28 +117,56 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
Data Extraction Goal
|
Data Extraction Goal
|
||||||
</Label>
|
</Label>
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, {
|
||||||
|
dataExtractionGoal: event.target.value,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
value={data.dataExtractionGoal}
|
value={data.dataExtractionGoal}
|
||||||
placeholder="What outputs are you looking to get?"
|
placeholder="What outputs are you looking to get?"
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DataSchema
|
<div className="space-y-2">
|
||||||
value={data.dataSchema}
|
<div className="flex gap-2">
|
||||||
onChange={() => {
|
<Label className="text-xs text-slate-300">Data Schema</Label>
|
||||||
if (!editable) return;
|
<Checkbox
|
||||||
// TODO
|
checked={data.dataSchema !== "null"}
|
||||||
}}
|
onCheckedChange={(checked) => {
|
||||||
/>
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, {
|
||||||
|
dataSchema: checked ? "{}" : "null",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.dataSchema !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={data.dataSchema}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { dataSchema: value });
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="limits">
|
<AccordionItem value="limits">
|
||||||
<AccordionTrigger>Limits</AccordionTrigger>
|
<AccordionTrigger>Limits</AccordionTrigger>
|
||||||
<AccordionContent className="pl-[1.5rem] pr-1">
|
<AccordionContent className="pl-[1.5rem] pr-1 pt-1">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs font-normal text-slate-300">
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
@@ -127,10 +176,15 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
className="nopan w-44"
|
className="nopan w-44"
|
||||||
|
min="0"
|
||||||
value={data.maxRetries ?? 0}
|
value={data.maxRetries ?? 0}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, {
|
||||||
|
maxRetries: Number(event.target.value),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,10 +196,15 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
className="nopan w-44"
|
className="nopan w-44"
|
||||||
|
min="0"
|
||||||
value={data.maxStepsOverride ?? 0}
|
value={data.maxStepsOverride ?? 0}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, {
|
||||||
|
maxStepsOverride: Number(event.target.value),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,20 +215,49 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<div className="w-44">
|
<div className="w-44">
|
||||||
<Switch
|
<Switch
|
||||||
checked={data.allowDownloads}
|
checked={data.allowDownloads}
|
||||||
onCheckedChange={() => {
|
onCheckedChange={(checked) => {
|
||||||
if (!editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { allowDownloads: checked });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TaskNodeErrorMapping
|
<div className="space-y-2">
|
||||||
value={data.errorCodeMapping}
|
<div className="flex gap-2">
|
||||||
onChange={() => {
|
<Label className="text-xs font-normal text-slate-300">
|
||||||
if (!editable) return;
|
Error Messages
|
||||||
// TODO
|
</Label>
|
||||||
}}
|
<Checkbox
|
||||||
/>
|
checked={data.errorCodeMapping !== "null"}
|
||||||
|
disabled={!editable}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, {
|
||||||
|
errorCodeMapping: checked ? "{}" : "null",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.errorCodeMapping !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={data.errorCodeMapping}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { errorCodeMapping: value });
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@@ -198,7 +286,11 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<ListBulletIcon className="h-6 w-6" />
|
<ListBulletIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">Task Block</span>
|
<span className="text-xs text-slate-400">Task Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,10 +298,28 @@ function TaskNode({ data }: NodeProps<TaskNode>) {
|
|||||||
<DotsHorizontalIcon className="h-6 w-6" />
|
<DotsHorizontalIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TaskNodeDisplayModeSwitch
|
<div className="flex justify-between">
|
||||||
value={displayMode}
|
<TaskNodeDisplayModeSwitch
|
||||||
onChange={setDisplayMode}
|
value={displayMode}
|
||||||
/>
|
onChange={setDisplayMode}
|
||||||
|
/>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button size="icon" variant="outline">
|
||||||
|
<MixerVerticalIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-72">
|
||||||
|
<TaskNodeParametersPanel
|
||||||
|
parameters={data.parameterKeys}
|
||||||
|
onParametersChange={(parameterKeys) => {
|
||||||
|
updateNodeData(id, { parameterKeys });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
{displayMode === "basic" && basicContent}
|
{displayMode === "basic" && basicContent}
|
||||||
{displayMode === "advanced" && advancedContent}
|
{displayMode === "advanced" && advancedContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
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,50 @@
|
|||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { useWorkflowParametersState } from "../../useWorkflowParametersState";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
parameters: Array<string>;
|
||||||
|
onParametersChange: (parameters: Array<string>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TaskNodeParametersPanel({ parameters, onParametersChange }: Props) {
|
||||||
|
const [workflowParameters] = useWorkflowParametersState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<header className="space-y-1">
|
||||||
|
<h1>Parameters</h1>
|
||||||
|
<span className="text-xs text-slate-300">
|
||||||
|
Check off the parameters you want to use in this task.
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{workflowParameters.map((workflowParameter) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={workflowParameter.key}
|
||||||
|
className="flex items-center gap-2 rounded-sm bg-slate-elevation1 px-3 py-2"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={parameters.includes(workflowParameter.key)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
onParametersChange([...parameters, workflowParameter.key]);
|
||||||
|
} else {
|
||||||
|
onParametersChange(
|
||||||
|
parameters.filter(
|
||||||
|
(parameter) => parameter !== workflowParameter.key,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs">{workflowParameter.key}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TaskNodeParametersPanel };
|
||||||
@@ -4,15 +4,30 @@ export type TaskNodeData = {
|
|||||||
url: string;
|
url: string;
|
||||||
navigationGoal: string;
|
navigationGoal: string;
|
||||||
dataExtractionGoal: string;
|
dataExtractionGoal: string;
|
||||||
errorCodeMapping: Record<string, string> | null;
|
errorCodeMapping: string;
|
||||||
dataSchema: Record<string, unknown> | null;
|
dataSchema: string;
|
||||||
maxRetries: number | null;
|
maxRetries: number | null;
|
||||||
maxStepsOverride: number | null;
|
maxStepsOverride: number | null;
|
||||||
allowDownloads: boolean;
|
allowDownloads: boolean;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
|
parameterKeys: Array<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TaskNode = Node<TaskNodeData, "task">;
|
export type TaskNode = Node<TaskNodeData, "task">;
|
||||||
|
|
||||||
export type TaskNodeDisplayMode = "basic" | "advanced";
|
export type TaskNodeDisplayMode = "basic" | "advanced";
|
||||||
|
|
||||||
|
export const taskNodeDefaultData: TaskNodeData = {
|
||||||
|
url: "",
|
||||||
|
navigationGoal: "",
|
||||||
|
dataExtractionGoal: "",
|
||||||
|
errorCodeMapping: "null",
|
||||||
|
dataSchema: "null",
|
||||||
|
maxRetries: null,
|
||||||
|
maxStepsOverride: null,
|
||||||
|
allowDownloads: false,
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
parameterKeys: [],
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { CursorTextIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
import { CursorTextIcon, DotsHorizontalIcon } from "@radix-ui/react-icons";
|
||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
import type { TextPromptNode } from "./types";
|
import type { TextPromptNode } from "./types";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { DataSchema } from "@/routes/workflows/components/DataSchema";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
|
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
const { editable } = data;
|
||||||
|
|
||||||
function TextPromptNode({ data }: NodeProps<TextPromptNode>) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -28,7 +33,11 @@ function TextPromptNode({ data }: NodeProps<TextPromptNode>) {
|
|||||||
<CursorTextIcon className="h-6 w-6" />
|
<CursorTextIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">Text Prompt Block</span>
|
<span className="text-xs text-slate-400">Text Prompt Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,9 +48,11 @@ function TextPromptNode({ data }: NodeProps<TextPromptNode>) {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs text-slate-300">Prompt</Label>
|
<Label className="text-xs text-slate-300">Prompt</Label>
|
||||||
<AutoResizingTextarea
|
<AutoResizingTextarea
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) return;
|
if (!editable) {
|
||||||
// TODO
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { prompt: event.target.value });
|
||||||
}}
|
}}
|
||||||
value={data.prompt}
|
value={data.prompt}
|
||||||
placeholder="What do you want to generate?"
|
placeholder="What do you want to generate?"
|
||||||
@@ -49,13 +60,37 @@ function TextPromptNode({ data }: NodeProps<TextPromptNode>) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<DataSchema
|
<div className="space-y-2">
|
||||||
value={data.jsonSchema}
|
<div className="flex gap-2">
|
||||||
onChange={() => {
|
<Label className="text-xs text-slate-300">Data Schema</Label>
|
||||||
if (!data.editable) return;
|
<Checkbox
|
||||||
// TODO
|
checked={data.jsonSchema !== "null"}
|
||||||
}}
|
onCheckedChange={(checked) => {
|
||||||
/>
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, {
|
||||||
|
jsonSchema: checked ? "{}" : "null",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.jsonSchema !== "null" && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
value={data.jsonSchema}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNodeData(id, { jsonSchema: value });
|
||||||
|
}}
|
||||||
|
className="nowheel nopan"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ import type { Node } from "@xyflow/react";
|
|||||||
|
|
||||||
export type TextPromptNodeData = {
|
export type TextPromptNodeData = {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
jsonSchema: Record<string, unknown> | null;
|
jsonSchema: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TextPromptNode = Node<TextPromptNodeData, "textPrompt">;
|
export type TextPromptNode = Node<TextPromptNodeData, "textPrompt">;
|
||||||
|
|
||||||
|
export const textPromptNodeDefaultData: TextPromptNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
prompt: "",
|
||||||
|
jsonSchema: "null",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Handle, NodeProps, Position } from "@xyflow/react";
|
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||||
import type { UploadNode } from "./types";
|
import type { UploadNode } from "./types";
|
||||||
import { DotsHorizontalIcon, UploadIcon } from "@radix-ui/react-icons";
|
import { DotsHorizontalIcon, UploadIcon } from "@radix-ui/react-icons";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
|
|
||||||
|
function UploadNode({ id, data }: NodeProps<UploadNode>) {
|
||||||
|
const { updateNodeData } = useReactFlow();
|
||||||
|
|
||||||
function UploadNode({ data }: NodeProps<UploadNode>) {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Handle
|
<Handle
|
||||||
@@ -26,7 +29,11 @@ function UploadNode({ data }: NodeProps<UploadNode>) {
|
|||||||
<UploadIcon className="h-6 w-6" />
|
<UploadIcon className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="max-w-64 truncate text-base">{data.label}</span>
|
<EditableNodeTitle
|
||||||
|
value={data.label}
|
||||||
|
editable={data.editable}
|
||||||
|
onChange={(value) => updateNodeData(id, { label: value })}
|
||||||
|
/>
|
||||||
<span className="text-xs text-slate-400">Upload Block</span>
|
<span className="text-xs text-slate-400">Upload Block</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,11 +46,11 @@ function UploadNode({ data }: NodeProps<UploadNode>) {
|
|||||||
<Label className="text-sm text-slate-400">File Path</Label>
|
<Label className="text-sm text-slate-400">File Path</Label>
|
||||||
<Input
|
<Input
|
||||||
value={data.path}
|
value={data.path}
|
||||||
onChange={() => {
|
onChange={(event) => {
|
||||||
if (!data.editable) {
|
if (!data.editable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO
|
updateNodeData(id, { path: event.target.value });
|
||||||
}}
|
}}
|
||||||
className="nopan"
|
className="nopan"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export type UploadNodeData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UploadNode = Node<UploadNodeData, "upload">;
|
export type UploadNode = Node<UploadNodeData, "upload">;
|
||||||
|
|
||||||
|
export const uploadNodeDefaultData: UploadNodeData = {
|
||||||
|
editable: true,
|
||||||
|
label: "",
|
||||||
|
path: "SKYVERN_DOWNLOAD_DIRECTORY",
|
||||||
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { cn } from "@/util/utils";
|
||||||
|
import { useLayoutEffect, useRef } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string;
|
||||||
|
editable: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function EditableNodeTitle({ value, editable, onChange, className }: Props) {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// size the textarea correctly on first render
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.current.style.width = `${ref.current.scrollWidth + 2}px`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function setSize() {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.current.style.width = "auto";
|
||||||
|
ref.current.style.width = `${ref.current.scrollWidth + 2}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
disabled={!editable}
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-fit min-w-fit max-w-64 border-0 px-0", className)}
|
||||||
|
onBlur={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
event.currentTarget.value = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(event.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.currentTarget.value = value;
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}
|
||||||
|
setSize();
|
||||||
|
}}
|
||||||
|
onInput={setSize}
|
||||||
|
defaultValue={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { EditableNodeTitle };
|
||||||
@@ -15,6 +15,8 @@ import type { UploadNode } from "./UploadNode/types";
|
|||||||
import { UploadNode as UploadNodeComponent } from "./UploadNode/UploadNode";
|
import { UploadNode as UploadNodeComponent } from "./UploadNode/UploadNode";
|
||||||
import type { DownloadNode } from "./DownloadNode/types";
|
import type { DownloadNode } from "./DownloadNode/types";
|
||||||
import { DownloadNode as DownloadNodeComponent } from "./DownloadNode/DownloadNode";
|
import { DownloadNode as DownloadNodeComponent } from "./DownloadNode/DownloadNode";
|
||||||
|
import type { NodeAdderNode } from "./NodeAdderNode/types";
|
||||||
|
import { NodeAdderNode as NodeAdderNodeComponent } from "./NodeAdderNode/NodeAdderNode";
|
||||||
|
|
||||||
export type AppNode =
|
export type AppNode =
|
||||||
| LoopNode
|
| LoopNode
|
||||||
@@ -24,7 +26,8 @@ export type AppNode =
|
|||||||
| CodeBlockNode
|
| CodeBlockNode
|
||||||
| FileParserNode
|
| FileParserNode
|
||||||
| UploadNode
|
| UploadNode
|
||||||
| DownloadNode;
|
| DownloadNode
|
||||||
|
| NodeAdderNode;
|
||||||
|
|
||||||
export const nodeTypes = {
|
export const nodeTypes = {
|
||||||
loop: memo(LoopNodeComponent),
|
loop: memo(LoopNodeComponent),
|
||||||
@@ -35,4 +38,5 @@ export const nodeTypes = {
|
|||||||
fileParser: memo(FileParserNodeComponent),
|
fileParser: memo(FileParserNodeComponent),
|
||||||
upload: memo(UploadNodeComponent),
|
upload: memo(UploadNodeComponent),
|
||||||
download: memo(DownloadNodeComponent),
|
download: memo(DownloadNodeComponent),
|
||||||
|
nodeAdder: memo(NodeAdderNodeComponent),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
|
import {
|
||||||
|
CodeIcon,
|
||||||
|
Cross2Icon,
|
||||||
|
CursorTextIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
EnvelopeClosedIcon,
|
||||||
|
FileIcon,
|
||||||
|
ListBulletIcon,
|
||||||
|
PlusIcon,
|
||||||
|
UpdateIcon,
|
||||||
|
UploadIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import { nodeTypes } from "../nodes";
|
||||||
|
import { AddNodeProps } from "../FlowRenderer";
|
||||||
|
|
||||||
|
const nodeLibraryItems: Array<{
|
||||||
|
nodeType: Exclude<keyof typeof nodeTypes, "nodeAdder">;
|
||||||
|
icon: JSX.Element;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
nodeType: "task",
|
||||||
|
icon: <ListBulletIcon className="h-6 w-6" />,
|
||||||
|
title: "Task Block",
|
||||||
|
description: "Takes actions or extracts information",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "loop",
|
||||||
|
icon: <UpdateIcon className="h-6 w-6" />,
|
||||||
|
title: "For Loop Block",
|
||||||
|
description: "Repeats nested elements",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "textPrompt",
|
||||||
|
icon: <CursorTextIcon className="h-6 w-6" />,
|
||||||
|
title: "Text Prompt Block",
|
||||||
|
description: "Generates AI response",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "sendEmail",
|
||||||
|
icon: <EnvelopeClosedIcon className="h-6 w-6" />,
|
||||||
|
title: "Send Email Block",
|
||||||
|
description: "Sends an email",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "codeBlock",
|
||||||
|
icon: <CodeIcon className="h-6 w-6" />,
|
||||||
|
title: "Code Block",
|
||||||
|
description: "Executes Python code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "fileParser",
|
||||||
|
icon: <FileIcon className="h-6 w-6" />,
|
||||||
|
title: "File Parser Block",
|
||||||
|
description: "Downloads and parses a file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "download",
|
||||||
|
icon: <DownloadIcon className="h-6 w-6" />,
|
||||||
|
title: "Download Block",
|
||||||
|
description: "Downloads a file from S3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nodeType: "upload",
|
||||||
|
icon: <UploadIcon className="h-6 w-6" />,
|
||||||
|
title: "Upload Block",
|
||||||
|
description: "Uploads a file to S3",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onNodeClick: (props: AddNodeProps) => void;
|
||||||
|
first?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) {
|
||||||
|
const workflowPanelData = useWorkflowPanelStore(
|
||||||
|
(state) => state.workflowPanelState.data,
|
||||||
|
);
|
||||||
|
const closeWorkflowPanel = useWorkflowPanelStore(
|
||||||
|
(state) => state.closeWorkflowPanel,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<header className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h1 className="text-lg">Node Library</h1>
|
||||||
|
{!first && (
|
||||||
|
<Cross2Icon
|
||||||
|
className="h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
closeWorkflowPanel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-slate-400">
|
||||||
|
{first
|
||||||
|
? "Click on the node type to add your first node"
|
||||||
|
: "Click on the node type you want to add"}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{nodeLibraryItems.map((item) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.nodeType}
|
||||||
|
className="flex cursor-pointer items-center justify-between rounded-sm bg-slate-elevation4 p-4 hover:bg-slate-elevation5"
|
||||||
|
onClick={() => {
|
||||||
|
onNodeClick({
|
||||||
|
nodeType: item.nodeType,
|
||||||
|
next: workflowPanelData?.next ?? null,
|
||||||
|
parent: workflowPanelData?.parent,
|
||||||
|
previous: workflowPanelData?.previous ?? null,
|
||||||
|
connectingEdgeType:
|
||||||
|
workflowPanelData?.connectingEdgeType ??
|
||||||
|
"edgeWithAddButton",
|
||||||
|
});
|
||||||
|
closeWorkflowPanel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="max-w-64 truncate text-base">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlusIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WorkflowNodeLibraryPanel };
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { WorkflowParameterValueType } from "../../types/workflowTypes";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ParametersState } from "../FlowRenderer";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: "workflow" | "credential";
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (value: ParametersState[number]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflowParameterTypeOptions = [
|
||||||
|
{ label: "string", value: WorkflowParameterValueType.String },
|
||||||
|
{ label: "number", value: WorkflowParameterValueType.Float },
|
||||||
|
{ label: "boolean", value: WorkflowParameterValueType.Boolean },
|
||||||
|
{ label: "file", value: WorkflowParameterValueType.FileURL },
|
||||||
|
{ label: "JSON", value: WorkflowParameterValueType.JSON },
|
||||||
|
];
|
||||||
|
|
||||||
|
function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
|
||||||
|
const [key, setKey] = useState("");
|
||||||
|
const [urlParameterKey, setUrlParameterKey] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [collectionId, setCollectionId] = useState("");
|
||||||
|
const [parameterType, setParameterType] =
|
||||||
|
useState<WorkflowParameterValueType>("string");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<header className="flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
Add {type === "workflow" ? "Workflow" : "Credential"} Parameter
|
||||||
|
</span>
|
||||||
|
<Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} />
|
||||||
|
</header>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Key</Label>
|
||||||
|
<Input value={key} onChange={(e) => setKey(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Description</Label>
|
||||||
|
<Input
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{type === "workflow" && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">Value Type</Label>
|
||||||
|
<Select
|
||||||
|
value={parameterType}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setParameterType(value as WorkflowParameterValueType)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{workflowParameterTypeOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{type === "credential" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">URL Parameter Key</Label>
|
||||||
|
<Input
|
||||||
|
value={urlParameterKey}
|
||||||
|
onChange={(e) => setUrlParameterKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Collection ID</Label>
|
||||||
|
<Input
|
||||||
|
value={collectionId}
|
||||||
|
onChange={(e) => setCollectionId(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (type === "workflow") {
|
||||||
|
onSave({
|
||||||
|
key,
|
||||||
|
parameterType: "workflow",
|
||||||
|
dataType: parameterType,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (type === "credential") {
|
||||||
|
onSave({
|
||||||
|
key,
|
||||||
|
parameterType: "credential",
|
||||||
|
collectionId,
|
||||||
|
urlParameterKey,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WorkflowParameterAddPanel };
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { WorkflowParameterValueType } from "../../types/workflowTypes";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ParametersState } from "../FlowRenderer";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: "workflow" | "credential";
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (value: ParametersState[number]) => void;
|
||||||
|
initialValues: ParametersState[number];
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflowParameterTypeOptions = [
|
||||||
|
{ label: "string", value: WorkflowParameterValueType.String },
|
||||||
|
{ label: "number", value: WorkflowParameterValueType.Float },
|
||||||
|
{ label: "boolean", value: WorkflowParameterValueType.Boolean },
|
||||||
|
{ label: "file", value: WorkflowParameterValueType.FileURL },
|
||||||
|
{ label: "JSON", value: WorkflowParameterValueType.JSON },
|
||||||
|
];
|
||||||
|
|
||||||
|
function WorkflowParameterEditPanel({
|
||||||
|
type,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
initialValues,
|
||||||
|
}: Props) {
|
||||||
|
const [key, setKey] = useState(initialValues.key);
|
||||||
|
const [urlParameterKey, setUrlParameterKey] = useState(
|
||||||
|
initialValues.parameterType === "credential"
|
||||||
|
? initialValues.urlParameterKey
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
const [description, setDescription] = useState(
|
||||||
|
initialValues.description || "",
|
||||||
|
);
|
||||||
|
const [collectionId, setCollectionId] = useState(
|
||||||
|
initialValues.parameterType === "credential"
|
||||||
|
? initialValues.collectionId
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
const [parameterType, setParameterType] =
|
||||||
|
useState<WorkflowParameterValueType>(
|
||||||
|
initialValues.parameterType === "workflow"
|
||||||
|
? initialValues.dataType
|
||||||
|
: "string",
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<header className="flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
Edit {type === "workflow" ? "Workflow" : "Credential"} Parameter
|
||||||
|
</span>
|
||||||
|
<Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} />
|
||||||
|
</header>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Key</Label>
|
||||||
|
<Input value={key} onChange={(e) => setKey(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Description</Label>
|
||||||
|
<Input
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{type === "workflow" && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">Value Type</Label>
|
||||||
|
<Select
|
||||||
|
value={parameterType}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setParameterType(value as WorkflowParameterValueType)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{workflowParameterTypeOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{type === "credential" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">URL Parameter Key</Label>
|
||||||
|
<Input
|
||||||
|
value={urlParameterKey}
|
||||||
|
onChange={(e) => setUrlParameterKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs text-slate-300">Collection ID</Label>
|
||||||
|
<Input
|
||||||
|
value={collectionId}
|
||||||
|
onChange={(e) => setCollectionId(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (type === "workflow") {
|
||||||
|
onSave({
|
||||||
|
key,
|
||||||
|
parameterType: "workflow",
|
||||||
|
dataType: parameterType,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (type === "credential") {
|
||||||
|
onSave({
|
||||||
|
key,
|
||||||
|
parameterType: "credential",
|
||||||
|
urlParameterKey,
|
||||||
|
collectionId,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WorkflowParameterEditPanel };
|
||||||
@@ -1,45 +1,227 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useState } from "react";
|
||||||
import { useWorkflowQuery } from "../../hooks/useWorkflowQuery";
|
import { useWorkflowParametersState } from "../useWorkflowParametersState";
|
||||||
|
import { WorkflowParameterAddPanel } from "./WorkflowParameterAddPanel";
|
||||||
|
import { ParametersState } from "../FlowRenderer";
|
||||||
|
import { WorkflowParameterEditPanel } from "./WorkflowParameterEditPanel";
|
||||||
|
import { MixerVerticalIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { GarbageIcon } from "@/components/icons/GarbageIcon";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DialogClose } from "@radix-ui/react-dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
||||||
|
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
||||||
|
|
||||||
function WorkflowParametersPanel() {
|
function WorkflowParametersPanel() {
|
||||||
const { workflowPermanentId } = useParams();
|
const [workflowParameters, setWorkflowParameters] =
|
||||||
|
useWorkflowParametersState();
|
||||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
const [operationPanelState, setOperationPanelState] = useState<{
|
||||||
workflowPermanentId,
|
active: boolean;
|
||||||
|
operation: "add" | "edit";
|
||||||
|
parameter?: ParametersState[number] | null;
|
||||||
|
type: "workflow" | "credential";
|
||||||
|
}>({
|
||||||
|
active: false,
|
||||||
|
operation: "add",
|
||||||
|
parameter: null,
|
||||||
|
type: "workflow",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || !workflow) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowParameters = workflow.workflow_definition.parameters.filter(
|
|
||||||
(parameter) => parameter.parameter_type === "workflow",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="relative w-[25rem] rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
|
||||||
<header>
|
<div className="space-y-4">
|
||||||
<h1 className="text-lg">Workflow Parameters</h1>
|
<header>
|
||||||
<span className="text-sm text-slate-400">
|
<h1 className="text-lg">Workflow Parameters</h1>
|
||||||
Create placeholder values that you can link in nodes. You will be
|
<span className="text-sm text-slate-400">
|
||||||
prompted to fill them in before running your workflow.
|
Create placeholder values that you can link in nodes. You will be
|
||||||
</span>
|
prompted to fill them in before running your workflow.
|
||||||
</header>
|
</span>
|
||||||
<section className="space-y-2">
|
</header>
|
||||||
{workflowParameters.map((parameter) => {
|
<DropdownMenu>
|
||||||
return (
|
<DropdownMenuTrigger asChild>
|
||||||
<div
|
<Button className="w-full">
|
||||||
key={parameter.key}
|
<PlusIcon className="mr-2 h-6 w-6" />
|
||||||
className="flex items-center gap-4 rounded-md bg-slate-elevation1 px-3 py-2"
|
Add Parameter
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-60">
|
||||||
|
<DropdownMenuLabel>Add Parameter</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setOperationPanelState({
|
||||||
|
active: true,
|
||||||
|
operation: "add",
|
||||||
|
type: "workflow",
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-sm">{parameter.key}</span>
|
Workflow Parameter
|
||||||
<span className="text-sm text-slate-400">
|
</DropdownMenuItem>
|
||||||
{parameter.workflow_parameter_type}
|
<DropdownMenuItem
|
||||||
</span>
|
onClick={() => {
|
||||||
|
setOperationPanelState({
|
||||||
|
active: true,
|
||||||
|
operation: "add",
|
||||||
|
type: "credential",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Credential Parameter
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<section className="space-y-2">
|
||||||
|
{workflowParameters.map((parameter) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={parameter.key}
|
||||||
|
className="flex items-center justify-between rounded-md bg-slate-elevation1 px-3 py-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-sm">{parameter.key}</span>
|
||||||
|
{parameter.parameterType === "workflow" ? (
|
||||||
|
<span className="text-sm text-slate-400">
|
||||||
|
{parameter.dataType}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-slate-400">
|
||||||
|
{parameter.parameterType}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MixerVerticalIcon
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setOperationPanelState({
|
||||||
|
active: true,
|
||||||
|
operation: "edit",
|
||||||
|
parameter: parameter,
|
||||||
|
type: parameter.parameterType,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger>
|
||||||
|
<GarbageIcon className="size-4 cursor-pointer text-destructive-foreground text-red-600" />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Are you sure?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
This parameter will be deleted.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setWorkflowParameters(
|
||||||
|
workflowParameters.filter(
|
||||||
|
(p) => p.key !== parameter.key,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{operationPanelState.active && (
|
||||||
|
<div
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top: 0,
|
||||||
|
left: -1 * (WORKFLOW_EDIT_PANEL_WIDTH + WORKFLOW_EDIT_PANEL_GAP),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{operationPanelState.operation === "add" && (
|
||||||
|
<div className="w-80 rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
|
||||||
|
<WorkflowParameterAddPanel
|
||||||
|
type={operationPanelState.type}
|
||||||
|
onSave={(parameter) => {
|
||||||
|
setWorkflowParameters([...workflowParameters, parameter]);
|
||||||
|
setOperationPanelState({
|
||||||
|
active: false,
|
||||||
|
operation: "add",
|
||||||
|
type: "workflow",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setOperationPanelState({
|
||||||
|
active: false,
|
||||||
|
operation: "add",
|
||||||
|
type: "workflow",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
{operationPanelState.operation === "edit" &&
|
||||||
</section>
|
operationPanelState.parameter && (
|
||||||
|
<div className="w-80 rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
|
||||||
|
<WorkflowParameterEditPanel
|
||||||
|
type={operationPanelState.type}
|
||||||
|
initialValues={operationPanelState.parameter}
|
||||||
|
onSave={(editedParameter) => {
|
||||||
|
setWorkflowParameters(
|
||||||
|
workflowParameters.map((parameter) => {
|
||||||
|
if (
|
||||||
|
parameter.key === operationPanelState.parameter?.key
|
||||||
|
) {
|
||||||
|
return editedParameter;
|
||||||
|
}
|
||||||
|
return parameter;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setOperationPanelState({
|
||||||
|
active: false,
|
||||||
|
operation: "edit",
|
||||||
|
parameter: null,
|
||||||
|
type: "workflow",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
setOperationPanelState({
|
||||||
|
active: false,
|
||||||
|
operation: "edit",
|
||||||
|
parameter: null,
|
||||||
|
type: "workflow",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext";
|
||||||
|
|
||||||
|
function useWorkflowParametersState() {
|
||||||
|
const value = useContext(WorkflowParametersStateContext);
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"useWorkflowParametersState must be used within a WorkflowParametersStateProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useWorkflowParametersState };
|
||||||
@@ -2,6 +2,18 @@ import { Edge } from "@xyflow/react";
|
|||||||
import { AppNode } from "./nodes";
|
import { AppNode } from "./nodes";
|
||||||
import Dagre from "@dagrejs/dagre";
|
import Dagre from "@dagrejs/dagre";
|
||||||
import type { WorkflowBlock } from "../types/workflowTypes";
|
import type { WorkflowBlock } from "../types/workflowTypes";
|
||||||
|
import { nodeTypes } from "./nodes";
|
||||||
|
import { taskNodeDefaultData } from "./nodes/TaskNode/types";
|
||||||
|
import { LoopNode, loopNodeDefaultData } from "./nodes/LoopNode/types";
|
||||||
|
import { codeBlockNodeDefaultData } from "./nodes/CodeBlockNode/types";
|
||||||
|
import { downloadNodeDefaultData } from "./nodes/DownloadNode/types";
|
||||||
|
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
||||||
|
import { sendEmailNodeDefaultData } from "./nodes/SendEmailNode/types";
|
||||||
|
import { textPromptNodeDefaultData } from "./nodes/TextPromptNode/types";
|
||||||
|
import { fileParserNodeDefaultData } from "./nodes/FileParserNode/types";
|
||||||
|
import { BlockYAML } from "../types/workflowYamlTypes";
|
||||||
|
import { NodeAdderNode } from "./nodes/NodeAdderNode/types";
|
||||||
|
import { REACT_FLOW_EDGE_Z_INDEX } from "./constants";
|
||||||
|
|
||||||
function layoutUtil(
|
function layoutUtil(
|
||||||
nodes: Array<AppNode>,
|
nodes: Array<AppNode>,
|
||||||
@@ -52,8 +64,12 @@ function layout(
|
|||||||
(node) => node.id === edge.source || node.id === edge.target,
|
(node) => node.id === edge.source || node.id === edge.target,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const maxChildWidth = Math.max(
|
||||||
|
...childNodes.map((node) => node.measured?.width ?? 0),
|
||||||
|
);
|
||||||
|
const loopNodeWidth = 60 * 16; // 60 rem
|
||||||
const layouted = layoutUtil(childNodes, childEdges, {
|
const layouted = layoutUtil(childNodes, childEdges, {
|
||||||
marginx: 240,
|
marginx: (loopNodeWidth - maxChildWidth) / 2,
|
||||||
marginy: 200,
|
marginy: 200,
|
||||||
});
|
});
|
||||||
loopNodeChildren[index] = layouted.nodes;
|
loopNodeChildren[index] = layouted.nodes;
|
||||||
@@ -75,6 +91,8 @@ function convertToNode(
|
|||||||
): AppNode {
|
): AppNode {
|
||||||
const common = {
|
const common = {
|
||||||
draggable: false,
|
draggable: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
connectable: false,
|
||||||
};
|
};
|
||||||
switch (block.block_type) {
|
switch (block.block_type) {
|
||||||
case "task": {
|
case "task": {
|
||||||
@@ -84,17 +102,17 @@ function convertToNode(
|
|||||||
type: "task",
|
type: "task",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
url: block.url ?? "",
|
url: block.url ?? "",
|
||||||
navigationGoal: block.navigation_goal ?? "",
|
navigationGoal: block.navigation_goal ?? "",
|
||||||
dataExtractionGoal: block.data_extraction_goal ?? "",
|
dataExtractionGoal: block.data_extraction_goal ?? "",
|
||||||
dataSchema: block.data_schema ?? null,
|
dataSchema: JSON.stringify(block.data_schema, null, 2),
|
||||||
errorCodeMapping: block.error_code_mapping ?? null,
|
errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
|
||||||
allowDownloads: block.complete_on_download ?? false,
|
allowDownloads: block.complete_on_download ?? false,
|
||||||
maxRetries: block.max_retries ?? null,
|
maxRetries: block.max_retries ?? null,
|
||||||
maxStepsOverride: block.max_steps_per_run ?? null,
|
maxStepsOverride: block.max_steps_per_run ?? null,
|
||||||
|
parameterKeys: block.parameters.map((p) => p.key),
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "code": {
|
case "code": {
|
||||||
@@ -104,10 +122,9 @@ function convertToNode(
|
|||||||
type: "codeBlock",
|
type: "codeBlock",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
code: block.code,
|
code: block.code,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "send_email": {
|
case "send_email": {
|
||||||
@@ -117,13 +134,13 @@ function convertToNode(
|
|||||||
type: "sendEmail",
|
type: "sendEmail",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
body: block.body,
|
body: block.body,
|
||||||
fileAttachments: block.file_attachments,
|
fileAttachments: block.file_attachments.join(", "),
|
||||||
recipients: block.recipients,
|
recipients: block.recipients.join(", "),
|
||||||
subject: block.subject,
|
subject: block.subject,
|
||||||
|
sender: block.sender,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "text_prompt": {
|
case "text_prompt": {
|
||||||
@@ -133,11 +150,10 @@ function convertToNode(
|
|||||||
type: "textPrompt",
|
type: "textPrompt",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
prompt: block.prompt,
|
prompt: block.prompt,
|
||||||
jsonSchema: block.json_schema ?? null,
|
jsonSchema: JSON.stringify(block.json_schema, null, 2),
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "for_loop": {
|
case "for_loop": {
|
||||||
@@ -147,10 +163,9 @@ function convertToNode(
|
|||||||
type: "loop",
|
type: "loop",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
loopValue: block.loop_over.key,
|
loopValue: block.loop_over.key,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "file_url_parser": {
|
case "file_url_parser": {
|
||||||
@@ -160,10 +175,9 @@ function convertToNode(
|
|||||||
type: "fileParser",
|
type: "fileParser",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
fileUrl: block.file_url,
|
fileUrl: block.file_url,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,10 +188,9 @@ function convertToNode(
|
|||||||
type: "download",
|
type: "download",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
url: block.url,
|
url: block.url,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,10 +201,9 @@ function convertToNode(
|
|||||||
type: "upload",
|
type: "upload",
|
||||||
data: {
|
data: {
|
||||||
label: block.label,
|
label: block.label,
|
||||||
editable: false,
|
editable: true,
|
||||||
path: block.path,
|
path: block.path,
|
||||||
},
|
},
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,22 +222,274 @@ function getElements(
|
|||||||
nodes.push(convertToNode({ id, parentId }, block));
|
nodes.push(convertToNode({ id, parentId }, block));
|
||||||
if (block.block_type === "for_loop") {
|
if (block.block_type === "for_loop") {
|
||||||
const subElements = getElements(block.loop_blocks, id);
|
const subElements = getElements(block.loop_blocks, id);
|
||||||
|
if (subElements.nodes.length === 0) {
|
||||||
|
nodes.push({
|
||||||
|
id: `${id}-nodeAdder`,
|
||||||
|
type: "nodeAdder",
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {},
|
||||||
|
draggable: false,
|
||||||
|
connectable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
nodes.push(...subElements.nodes);
|
nodes.push(...subElements.nodes);
|
||||||
edges.push(...subElements.edges);
|
edges.push(...subElements.edges);
|
||||||
}
|
}
|
||||||
if (index !== blocks.length - 1) {
|
if (index !== blocks.length - 1) {
|
||||||
edges.push({
|
edges.push({
|
||||||
id: `edge-${id}-${nextId}`,
|
id: `edge-${id}-${nextId}`,
|
||||||
|
type: "edgeWithAddButton",
|
||||||
source: id,
|
source: id,
|
||||||
target: nextId,
|
target: nextId,
|
||||||
style: {
|
style: {
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
},
|
},
|
||||||
|
zIndex: REACT_FLOW_EDGE_Z_INDEX,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
edges.push({
|
||||||
|
id: "edge-nodeAdder",
|
||||||
|
type: "default",
|
||||||
|
source: nodes[nodes.length - 1]!.id,
|
||||||
|
target: "nodeAdder",
|
||||||
|
style: {
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
nodes.push({
|
||||||
|
id: "nodeAdder",
|
||||||
|
type: "nodeAdder",
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: {},
|
||||||
|
draggable: false,
|
||||||
|
connectable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { nodes, edges };
|
return { nodes, edges };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getElements, layout };
|
function createNode(
|
||||||
|
identifiers: { id: string; parentId?: string },
|
||||||
|
nodeType: Exclude<keyof typeof nodeTypes, "nodeAdder">,
|
||||||
|
labelPostfix: string, // unique label requirement
|
||||||
|
): AppNode {
|
||||||
|
const label = "Block " + labelPostfix;
|
||||||
|
const common = {
|
||||||
|
draggable: false,
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
};
|
||||||
|
switch (nodeType) {
|
||||||
|
case "task": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "task",
|
||||||
|
data: {
|
||||||
|
...taskNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "loop": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "loop",
|
||||||
|
data: {
|
||||||
|
...loopNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "codeBlock": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "codeBlock",
|
||||||
|
data: {
|
||||||
|
...codeBlockNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "download": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "download",
|
||||||
|
data: {
|
||||||
|
...downloadNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "upload": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "upload",
|
||||||
|
data: {
|
||||||
|
...uploadNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "sendEmail": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "sendEmail",
|
||||||
|
data: {
|
||||||
|
...sendEmailNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "textPrompt": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "textPrompt",
|
||||||
|
data: {
|
||||||
|
...textPromptNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "fileParser": {
|
||||||
|
return {
|
||||||
|
...identifiers,
|
||||||
|
...common,
|
||||||
|
type: "fileParser",
|
||||||
|
data: {
|
||||||
|
...fileParserNodeDefaultData,
|
||||||
|
label,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function JSONParseSafe(json: string): Record<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowBlock(
|
||||||
|
node: Exclude<AppNode, LoopNode | NodeAdderNode>,
|
||||||
|
): BlockYAML {
|
||||||
|
switch (node.type) {
|
||||||
|
case "task": {
|
||||||
|
return {
|
||||||
|
block_type: "task",
|
||||||
|
label: node.data.label,
|
||||||
|
url: node.data.url,
|
||||||
|
navigation_goal: node.data.navigationGoal,
|
||||||
|
data_extraction_goal: node.data.dataExtractionGoal,
|
||||||
|
data_schema: JSONParseSafe(node.data.dataSchema),
|
||||||
|
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
> | null,
|
||||||
|
max_retries: node.data.maxRetries ?? undefined,
|
||||||
|
max_steps_per_run: node.data.maxStepsOverride,
|
||||||
|
complete_on_download: node.data.allowDownloads,
|
||||||
|
parameter_keys: node.data.parameterKeys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "sendEmail": {
|
||||||
|
return {
|
||||||
|
block_type: "send_email",
|
||||||
|
label: node.data.label,
|
||||||
|
body: node.data.body,
|
||||||
|
file_attachments: node.data.fileAttachments.split(","),
|
||||||
|
recipients: node.data.recipients.split(","),
|
||||||
|
subject: node.data.subject,
|
||||||
|
sender: node.data.sender,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "codeBlock": {
|
||||||
|
return {
|
||||||
|
block_type: "code",
|
||||||
|
label: node.data.label,
|
||||||
|
code: node.data.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "download": {
|
||||||
|
return {
|
||||||
|
block_type: "download_to_s3",
|
||||||
|
label: node.data.label,
|
||||||
|
url: node.data.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "upload": {
|
||||||
|
return {
|
||||||
|
block_type: "upload_to_s3",
|
||||||
|
label: node.data.label,
|
||||||
|
path: node.data.path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "fileParser": {
|
||||||
|
return {
|
||||||
|
block_type: "file_url_parser",
|
||||||
|
label: node.data.label,
|
||||||
|
file_url: node.data.fileUrl,
|
||||||
|
file_type: "csv",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "textPrompt": {
|
||||||
|
return {
|
||||||
|
block_type: "text_prompt",
|
||||||
|
label: node.data.label,
|
||||||
|
llm_key: "",
|
||||||
|
prompt: node.data.prompt,
|
||||||
|
json_schema: JSONParseSafe(node.data.jsonSchema),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error("Invalid node type for getWorkflowBlock");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowBlocksUtil(nodes: Array<AppNode>): Array<BlockYAML> {
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
if (node.parentId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (node.type === "loop") {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
block_type: "for_loop",
|
||||||
|
label: node.data.label,
|
||||||
|
loop_over_parameter_key: node.data.loopValue,
|
||||||
|
loop_blocks: nodes
|
||||||
|
.filter((n) => n.parentId === node.id)
|
||||||
|
.map((n) => {
|
||||||
|
return getWorkflowBlock(
|
||||||
|
n as Exclude<AppNode, LoopNode | NodeAdderNode>,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
getWorkflowBlock(node as Exclude<AppNode, LoopNode | NodeAdderNode>),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkflowBlocks(nodes: Array<AppNode>): Array<BlockYAML> {
|
||||||
|
return getWorkflowBlocksUtil(
|
||||||
|
nodes.filter((node) => node.type !== "nodeAdder"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getElements, layout, createNode, getWorkflowBlocks };
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export type WorkflowBlock =
|
|||||||
| FileURLParserBlock;
|
| FileURLParserBlock;
|
||||||
|
|
||||||
export type WorkflowDefinition = {
|
export type WorkflowDefinition = {
|
||||||
parameters: Array<WorkflowParameter>;
|
parameters: Array<Parameter>;
|
||||||
blocks: Array<WorkflowBlock>;
|
blocks: Array<WorkflowBlock>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -211,6 +211,7 @@ export type WorkflowApiResponse = {
|
|||||||
workflow_definition: WorkflowDefinition;
|
workflow_definition: WorkflowDefinition;
|
||||||
proxy_location: string;
|
proxy_location: string;
|
||||||
webhook_callback_url: string;
|
webhook_callback_url: string;
|
||||||
|
totp_verification_url: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
modified_at: string;
|
modified_at: string;
|
||||||
deleted_at: string | null;
|
deleted_at: string | null;
|
||||||
|
|||||||
133
skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
Normal file
133
skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
export type WorkflowCreateYAMLRequest = {
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
proxy_location?: string | null;
|
||||||
|
webhook_callback_url?: string | null;
|
||||||
|
totp_verification_url?: string | null;
|
||||||
|
workflow_definition: WorkflowDefinitionYAML;
|
||||||
|
is_saved_task?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowDefinitionYAML = {
|
||||||
|
parameters: Array<ParameterYAML>;
|
||||||
|
blocks: Array<BlockYAML>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParameterYAML =
|
||||||
|
| WorkflowParameterYAML
|
||||||
|
| BitwardenLoginCredentialParameterYAML;
|
||||||
|
|
||||||
|
export type ParameterYAMLBase = {
|
||||||
|
parameter_type: string;
|
||||||
|
key: string;
|
||||||
|
description?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkflowParameterYAML = ParameterYAMLBase & {
|
||||||
|
parameter_type: "workflow";
|
||||||
|
workflow_parameter_type: string;
|
||||||
|
default_value: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BitwardenLoginCredentialParameterYAML = ParameterYAMLBase & {
|
||||||
|
parameter_type: "bitwarden_login_credential";
|
||||||
|
bitwarden_collection_id: string;
|
||||||
|
url_parameter_key: string;
|
||||||
|
bitwarden_client_id_aws_secret_key: "SKYVERN_BITWARDEN_CLIENT_ID";
|
||||||
|
bitwarden_client_secret_aws_secret_key: "SKYVERN_BITWARDEN_CLIENT_SECRET";
|
||||||
|
bitwarden_master_password_aws_secret_key: "SKYVERN_BITWARDEN_MASTER_PASSWORD";
|
||||||
|
};
|
||||||
|
|
||||||
|
const BlockTypes = {
|
||||||
|
TASK: "task",
|
||||||
|
FOR_LOOP: "for_loop",
|
||||||
|
CODE: "code",
|
||||||
|
TEXT_PROMPT: "text_prompt",
|
||||||
|
DOWNLOAD_TO_S3: "download_to_s3",
|
||||||
|
UPLOAD_TO_S3: "upload_to_s3",
|
||||||
|
SEND_EMAIL: "send_email",
|
||||||
|
FILE_URL_PARSER: "file_url_parser",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||||
|
|
||||||
|
export type BlockYAML =
|
||||||
|
| TaskBlockYAML
|
||||||
|
| CodeBlockYAML
|
||||||
|
| TextPromptBlockYAML
|
||||||
|
| DownloadToS3BlockYAML
|
||||||
|
| UploadToS3BlockYAML
|
||||||
|
| SendEmailBlockYAML
|
||||||
|
| FileUrlParserBlockYAML
|
||||||
|
| ForLoopBlockYAML;
|
||||||
|
|
||||||
|
export type BlockYAMLBase = {
|
||||||
|
block_type: BlockType;
|
||||||
|
label: string;
|
||||||
|
continue_on_failure?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaskBlockYAML = BlockYAMLBase & {
|
||||||
|
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;
|
||||||
|
parameter_keys?: Array<string> | null;
|
||||||
|
complete_on_download?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CodeBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "code";
|
||||||
|
code: string;
|
||||||
|
parameter_keys?: Array<string> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TextPromptBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "text_prompt";
|
||||||
|
llm_key: string;
|
||||||
|
prompt: string;
|
||||||
|
json_schema?: Record<string, unknown> | null;
|
||||||
|
parameter_keys?: Array<string> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DownloadToS3BlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "download_to_s3";
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadToS3BlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "upload_to_s3";
|
||||||
|
path?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SendEmailBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "send_email";
|
||||||
|
|
||||||
|
smtp_host_secret_parameter_key?: string;
|
||||||
|
smtp_port_secret_parameter_key?: string;
|
||||||
|
smtp_username_secret_parameter_key?: string;
|
||||||
|
smtp_password_secret_parameter_key?: string;
|
||||||
|
|
||||||
|
sender: string;
|
||||||
|
recipients: Array<string>;
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
file_attachments?: Array<string> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FileUrlParserBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "file_url_parser";
|
||||||
|
file_url: string;
|
||||||
|
file_type: "csv";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ForLoopBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "for_loop";
|
||||||
|
loop_over_parameter_key: string;
|
||||||
|
loop_blocks: Array<BlockYAML>;
|
||||||
|
};
|
||||||
49
skyvern-frontend/src/store/WorkflowPanelStore.ts
Normal file
49
skyvern-frontend/src/store/WorkflowPanelStore.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type WorkflowPanelState = {
|
||||||
|
active: boolean;
|
||||||
|
content: "parameters" | "nodeLibrary";
|
||||||
|
data?: {
|
||||||
|
previous?: string | null;
|
||||||
|
next?: string | null;
|
||||||
|
parent?: string;
|
||||||
|
connectingEdgeType?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkflowPanelStore = {
|
||||||
|
workflowPanelState: WorkflowPanelState;
|
||||||
|
closeWorkflowPanel: () => void;
|
||||||
|
setWorkflowPanelState: (state: WorkflowPanelState) => void;
|
||||||
|
toggleWorkflowPanel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWorkflowPanelStore = create<WorkflowPanelStore>((set, get) => {
|
||||||
|
return {
|
||||||
|
workflowPanelState: {
|
||||||
|
active: false,
|
||||||
|
content: "parameters",
|
||||||
|
},
|
||||||
|
setWorkflowPanelState: (workflowPanelState: WorkflowPanelState) => {
|
||||||
|
set({ workflowPanelState });
|
||||||
|
},
|
||||||
|
closeWorkflowPanel: () => {
|
||||||
|
set({
|
||||||
|
workflowPanelState: {
|
||||||
|
...get().workflowPanelState,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleWorkflowPanel: () => {
|
||||||
|
set({
|
||||||
|
workflowPanelState: {
|
||||||
|
...get().workflowPanelState,
|
||||||
|
active: !get().workflowPanelState.active,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export { useWorkflowPanelStore };
|
||||||
Reference in New Issue
Block a user