mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 00:03:09 +00:00
Merge branch 'main' into alp/teams_scripts
This commit is contained in:
@@ -35,7 +35,7 @@ jobs:
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: benchmark
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
run: deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
|
||||
-c
|
||||
|
||||
@@ -46,6 +46,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read EE repo commit hash
|
||||
@@ -108,6 +109,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read EE repo commit hash
|
||||
@@ -205,7 +207,6 @@ jobs:
|
||||
ARCH: amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- run: |
|
||||
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
|
||||
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
|
||||
@@ -291,6 +292,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Prepare test run
|
||||
if: ${{ ! startsWith(github.ref, 'refs/tags/v') }}
|
||||
@@ -354,7 +356,7 @@ jobs:
|
||||
verify_ee_image_vulnerabilities:
|
||||
runs-on: ubicloud
|
||||
needs: [tag_latest_ee]
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }} && (github.event_name != 'workflow_dispatch')
|
||||
if: startsWith(github.ref, 'refs/tags/v') && (github.event_name != 'workflow_dispatch')
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -403,6 +405,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v2
|
||||
@@ -431,6 +434,11 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update Dockerfile image reference
|
||||
run: |
|
||||
sed -i 's|FROM ghcr.io/windmill-labs/windmill-ee:dev|FROM ghcr.io/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}|' ./docker/DockerfileNsjail
|
||||
cat ./docker/DockerfileNsjail | grep "FROM"
|
||||
|
||||
- name: Build and push publicly ee
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
@@ -542,6 +550,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v2
|
||||
|
||||
@@ -1,5 +1,70 @@
|
||||
# Changelog
|
||||
|
||||
## [1.455.1](https://github.com/windmill-labs/windmill/compare/v1.455.0...v1.455.1) (2025-01-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pin malachite version ([b74d3fd](https://github.com/windmill-labs/windmill/commit/b74d3fd6e08ea991f7a7360b79d54a45a8ee58be))
|
||||
|
||||
## [1.455.0](https://github.com/windmill-labs/windmill/compare/v1.454.1...v1.455.0) (2025-01-31)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** add --parallel option to push + display timings ([73dbb8f](https://github.com/windmill-labs/windmill/commit/73dbb8fb80f56942d35e5b5cda9715bba5d5c98f))
|
||||
* duckdb connection settings with azure blob storage ([#5180](https://github.com/windmill-labs/windmill/issues/5180)) ([8ba0f3a](https://github.com/windmill-labs/windmill/commit/8ba0f3addd0a02dba87c30739b44f524019a5e5a))
|
||||
* polars connection settings with azure blob storage ([#5175](https://github.com/windmill-labs/windmill/issues/5175)) ([8b65f1b](https://github.com/windmill-labs/windmill/commit/8b65f1bc3829d6b02bf38bf1449d3061ef62a08c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* capture panel never stopping + http saved config ([#5179](https://github.com/windmill-labs/windmill/issues/5179)) ([ff3339d](https://github.com/windmill-labs/windmill/commit/ff3339dc4e9ea4641b847fa5ef51bedcf6a3a429))
|
||||
* restart zombie job correctly handle concurrency limits ([#5181](https://github.com/windmill-labs/windmill/issues/5181)) ([8a93418](https://github.com/windmill-labs/windmill/commit/8a93418c25d180859bda76b9218e750e4b407fed))
|
||||
|
||||
## [1.454.1](https://github.com/windmill-labs/windmill/compare/v1.454.0...v1.454.1) (2025-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix codebase script execution ([b1155a9](https://github.com/windmill-labs/windmill/commit/b1155a9012c726c1b1c3f1a62f3aa96b8504681c))
|
||||
|
||||
## [1.454.0](https://github.com/windmill-labs/windmill/compare/v1.453.1...v1.454.0) (2025-01-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* history for flow inputs ([#5117](https://github.com/windmill-labs/windmill/issues/5117)) ([cd44056](https://github.com/windmill-labs/windmill/commit/cd440564d29aff39f6dc30ecbf0055696b07cf12))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix app form password handling ([4812c0c](https://github.com/windmill-labs/windmill/commit/4812c0cc9e46fdb5944f7010efdf617f2fd23cff))
|
||||
* improve codebase support ([c8cc9d2](https://github.com/windmill-labs/windmill/commit/c8cc9d2941dda2fedb959e1156c4f9b4f7dfab43))
|
||||
|
||||
## [1.453.1](https://github.com/windmill-labs/windmill/compare/v1.453.0...v1.453.1) (2025-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** fetching job result (db schema) separatly if too big ([#5171](https://github.com/windmill-labs/windmill/issues/5171)) ([f29ee67](https://github.com/windmill-labs/windmill/commit/f29ee67d9d8547d7c4a79e14698b7d88a722e730))
|
||||
* **typescript-client:** fix typescript client mocked api behavior ([7f516f0](https://github.com/windmill-labs/windmill/commit/7f516f08682876f5a7ca409ec002cba6ec0c5c5b))
|
||||
|
||||
## [1.453.0](https://github.com/windmill-labs/windmill/compare/v1.452.1...v1.453.0) (2025-01-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* custom response headers ([#5156](https://github.com/windmill-labs/windmill/issues/5156)) ([5544d42](https://github.com/windmill-labs/windmill/commit/5544d42a2db652660051b5f2e05f8db0102400fa))
|
||||
* **python:** add `custom_wheels` directory to `PYTHONPATH` ([#5169](https://github.com/windmill-labs/windmill/issues/5169)) ([c60f8da](https://github.com/windmill-labs/windmill/commit/c60f8dacf191c5255ef8fca462d6d3d571b86b07))
|
||||
* websocket trigger allow returning messages ([#5168](https://github.com/windmill-labs/windmill/issues/5168)) ([487c273](https://github.com/windmill-labs/windmill/commit/487c273bfbc5bf1731947c922f32cd026cca47f9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* oracledb tag for native workers + client not working on arm systems ([#5162](https://github.com/windmill-labs/windmill/issues/5162)) ([a1579c1](https://github.com/windmill-labs/windmill/commit/a1579c1654a2ddf9f6fc8f05f62b37bb09303752))
|
||||
* **python:** fix uv can't find ssl certificates ([#5157](https://github.com/windmill-labs/windmill/issues/5157)) ([680fb18](https://github.com/windmill-labs/windmill/commit/680fb1852a192e384e23e2f5ceea473383f9f5aa))
|
||||
|
||||
## [1.452.1](https://github.com/windmill-labs/windmill/compare/v1.452.0...v1.452.1) (2025-01-27)
|
||||
|
||||
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", \n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "websocket_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "http_routes_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "kafka_used!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "nats_used!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1ef48cc430870ab6c062b046bdbf0a2db057141619e9818ddb904d7c242efec3"
|
||||
}
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
|
||||
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as \"payload!: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "payload: _",
|
||||
"name": "payload!: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
@@ -71,9 +71,9 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4"
|
||||
"hash": "5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd"
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, error = NULL\n WHERE workspace_id = $10 AND path = $11",
|
||||
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL\n WHERE workspace_id = $11 AND path = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -14,11 +14,12 @@
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "561b7935d687f6b9f3d6488f8489f55e6737ffa2d3716f803d32f8b68cc1e915"
|
||||
"hash": "63b42286804a3f3977235935d7ac200a07c5a4ad3a50051b73f382385b89466e"
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE queue SET running = false, started_at = null\n WHERE last_ping < now() - ($1 || ' seconds')::interval\n AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false RETURNING id, workspace_id, last_ping",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "last_ping",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "84576f6add15108ade2ed3a88185d67db17904859e5f01f1d578af4e1d062a85"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH zombie_jobs AS (\n UPDATE queue SET running = false, started_at = null\n WHERE last_ping < now() - ($1 || ' seconds')::interval\n AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false \n RETURNING id, workspace_id, last_ping\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id, workspace_id, last_ping FROM zombie_jobs",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "last_ping",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9a9b639611459659ae355a43f219c6da2c3d1e04d49306adaaaaf06e54ee8357"
|
||||
}
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
|
||||
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase LIKE '%.tar' as use_tar FROM script WHERE hash = $1 LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -52,8 +52,8 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "codebase: String",
|
||||
"type_info": "Varchar"
|
||||
"name": "use_tar",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -66,8 +66,8 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664"
|
||||
"hash": "d55eabd42559893bdf91c5fdab1ae58006013be26fcb9367b3c6830f38749799"
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload!: _\", trigger_extra as \"trigger_extra: _\" FROM capture WHERE id = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "trigger_kind: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email",
|
||||
"nats"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "payload!: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "trigger_extra: _",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7"
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, is_secret\n FROM variable\n WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_secret",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f3d3d105b76ec2eded521c3bb3735a3765c9aa2d103e3df85c245d85ba32b3b9"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT flow_version.value->>'early_return' as early_return\n FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 and flow.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "early_return",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "fc243af1bc70f04e28c006364d6dfdfc49981568e8c496fb6a163c499c3e4ad1"
|
||||
}
|
||||
Generated
+111
-99
@@ -718,9 +718,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sso"
|
||||
version = "1.56.0"
|
||||
version = "1.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12e057fdcb8842de9b83592a70f5b4da0ee10bc0ad278247da1425a742a444d7"
|
||||
checksum = "c54bab121fe1881a74c338c5f723d1592bf3b53167f80268a1274f404e1acc38"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -740,9 +740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-ssooidc"
|
||||
version = "1.57.1"
|
||||
version = "1.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a120ade4a44691b3c5c2ff2fa61b14ed331fdc218397f61ab48d66593012ae2a"
|
||||
checksum = "8c8234fd024f7ac61c4e44ea008029bde934250f371efe7d4a39708397b1080c"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -762,9 +762,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.57.0"
|
||||
version = "1.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115fd4fb663817ed595a5ee4f1649d7aacd861d47462323cb37576ce89271b93"
|
||||
checksum = "ba60e1d519d6f23a9df712c04fdeadd7872ac911c84b2f62a8bda92e129b7962"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -962,7 +962,7 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
@@ -1282,7 +1282,7 @@ dependencies = [
|
||||
"hex",
|
||||
"http 1.2.0",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-named-pipe",
|
||||
"hyper-util",
|
||||
"hyperlocal",
|
||||
@@ -1388,9 +1388,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
@@ -1708,9 +1708,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.52"
|
||||
version = "0.1.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
|
||||
checksum = "e24a03c8b52922d68a1589ad61032f2c1aa5a8158d2aa0d93c6e9534944bbad6"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -1941,7 +1941,7 @@ checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"once_cell",
|
||||
"winnow 0.6.25",
|
||||
"winnow 0.6.26",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2641,7 +2641,7 @@ dependencies = [
|
||||
"hickory-resolver",
|
||||
"http 1.2.0",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-rustls 0.27.5",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
@@ -2777,7 +2777,7 @@ checksum = "688175eed35e7b3053ec114227894ef24786855405d8844058a48bffa997d85a"
|
||||
dependencies = [
|
||||
"deno_core",
|
||||
"deno_native_certs",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-tokio-stream",
|
||||
"rustls-webpki 0.102.8",
|
||||
@@ -3766,9 +3766,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71393ecc86efbf00e4ca13953979ba8b94cfe549a4b74cc26d8b62f4d8feac2b"
|
||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
@@ -3958,9 +3958,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashify"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a194e6d22f060dada750b0c33c9c3d01dce0ba5212ea81e0a46834e065e932c"
|
||||
checksum = "f208758247e68e239acaa059e72e4ce1f30f2a4b6523f19c1b923d25b7e9cceb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4201,9 +4201,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.9.5"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
|
||||
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
@@ -4243,9 +4243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.5.2"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
|
||||
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -4269,7 +4269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -4301,9 +4301,9 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.2.0",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
@@ -4318,7 +4318,7 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793"
|
||||
dependencies = [
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -4346,7 +4346,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
@@ -4365,7 +4365,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -4382,7 +4382,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
@@ -4587,9 +4587,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.9"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
|
||||
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
@@ -5060,9 +5060,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mail-parser"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8f64d96d3235bb1390e3d8efd745deb66b85595ce4db431a3ddc0e841a6015"
|
||||
checksum = "187a2b93c4c8c32f552ee06c2d99915e575de2fc7e04b07891c9edfee5b8edd6"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"hashify",
|
||||
@@ -5078,7 +5078,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"gethostname",
|
||||
"mail-builder",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-pki-types",
|
||||
"smtp-proto",
|
||||
"tokio",
|
||||
@@ -5099,9 +5099,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "malachite-base"
|
||||
version = "0.4.18"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06f6d078bb4dad5b76f6a85737e6c9113667d199612a73eef375c56e75d5d4d5"
|
||||
checksum = "cc7d99f6446827fb878a005ea5cf7db1d78d236391733012216a9b8eb5dbd824"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"itertools 0.11.0",
|
||||
@@ -5124,9 +5124,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "malachite-nz"
|
||||
version = "0.4.18"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad61a09cde72dcdb8b2baaf3e71430c5bd94fb6753da89f17301a459f3e466db"
|
||||
checksum = "f36157fa6b969ca652feefe159e202ebd85683e57463074492a7ec3eaa468c97"
|
||||
dependencies = [
|
||||
"itertools 0.11.0",
|
||||
"libm",
|
||||
@@ -5135,9 +5135,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "malachite-q"
|
||||
version = "0.4.18"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4a9dfffb87667ae94e8320213d3f5419e3e37311bc6bf8f4d2ab9b44f3a535"
|
||||
checksum = "3dde29a4fcf7025f482745247951f7c5d0459671f59aedf09035b2e2c3746fdf"
|
||||
dependencies = [
|
||||
"itertools 0.11.0",
|
||||
"malachite-base",
|
||||
@@ -5666,7 +5666,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
"humantime",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"itertools 0.13.0",
|
||||
"md-5 0.10.6",
|
||||
"parking_lot",
|
||||
@@ -6427,7 +6427,7 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit 0.22.22",
|
||||
"toml_edit 0.22.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6710,7 +6710,7 @@ dependencies = [
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"socket2",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
@@ -6728,7 +6728,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"ring 0.17.8",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.11",
|
||||
@@ -6855,7 +6855,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
|
||||
dependencies = [
|
||||
"getrandom 0.3.0",
|
||||
"getrandom 0.3.1",
|
||||
"zerocopy 0.8.14",
|
||||
]
|
||||
|
||||
@@ -7118,7 +7118,7 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-rustls 0.27.5",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
@@ -7131,7 +7131,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
@@ -7397,9 +7397,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.21"
|
||||
version = "0.23.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
|
||||
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -7481,7 +7481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"socket2",
|
||||
"tokio",
|
||||
]
|
||||
@@ -7572,9 +7572,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
|
||||
[[package]]
|
||||
name = "ryu-js"
|
||||
@@ -7842,9 +7842,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.137"
|
||||
version = "1.0.138"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
|
||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
||||
dependencies = [
|
||||
"indexmap 2.7.1",
|
||||
"itoa",
|
||||
@@ -8377,7 +8377,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -9299,13 +9299,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.15.0"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
|
||||
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand 2.3.0",
|
||||
"getrandom 0.2.15",
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -9681,7 +9681,7 @@ version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
|
||||
dependencies = [
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -9793,7 +9793,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.22",
|
||||
"toml_edit 0.22.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9820,15 +9820,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.22"
|
||||
version = "0.22.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
|
||||
dependencies = [
|
||||
"indexmap 2.7.1",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.6.25",
|
||||
"winnow 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9846,7 +9846,7 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"hyper-timeout",
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
@@ -10346,9 +10346,9 @@ checksum = "2f322b60f6b9736017344fa0635d64be2f458fbc04eef65f6be22976dd1ffd5b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -10455,7 +10455,7 @@ dependencies = [
|
||||
"log",
|
||||
"native-tls",
|
||||
"once_cell",
|
||||
"rustls 0.23.21",
|
||||
"rustls 0.23.22",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -10779,9 +10779,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.7"
|
||||
version = "0.26.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
|
||||
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
@@ -10860,7 +10860,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -10890,6 +10890,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.12.1",
|
||||
"v8",
|
||||
"windmill-api",
|
||||
"windmill-api-client",
|
||||
"windmill-autoscaling",
|
||||
@@ -10902,7 +10903,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -10932,7 +10933,7 @@ dependencies = [
|
||||
"hf-hub",
|
||||
"hmac",
|
||||
"http 1.2.0",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"itertools 0.14.0",
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
@@ -10995,7 +10996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -11013,7 +11014,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -11026,7 +11027,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -11040,7 +11041,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -11060,7 +11061,7 @@ dependencies = [
|
||||
"git-version",
|
||||
"hex",
|
||||
"hmac",
|
||||
"hyper 1.5.2",
|
||||
"hyper 1.6.0",
|
||||
"indexmap 2.7.1",
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -11099,7 +11100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -11113,7 +11114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -11136,7 +11137,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -11148,7 +11149,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -11157,7 +11158,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11169,7 +11170,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -11181,7 +11182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -11193,7 +11194,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11205,7 +11206,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -11216,7 +11217,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -11227,12 +11228,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
"malachite",
|
||||
"malachite-bigint",
|
||||
"phf",
|
||||
"regex",
|
||||
"regex-lite",
|
||||
@@ -11245,7 +11248,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -11262,7 +11265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11274,7 +11277,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -11292,7 +11295,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.15",
|
||||
@@ -11314,7 +11317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -11324,7 +11327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11357,7 +11360,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -11367,7 +11370,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11642,9 +11645,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.25"
|
||||
version = "0.6.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310"
|
||||
checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
+8
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.452.1"
|
||||
version = "1.455.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -61,7 +61,7 @@ cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
|
||||
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
|
||||
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "enterprise", "parquet"]
|
||||
sqlx = ["windmill-worker/sqlx"]
|
||||
deno_core = ["windmill-worker/deno_core", "dep:deno_core"]
|
||||
deno_core = ["windmill-worker/deno_core", "dep:deno_core", "dep:v8"]
|
||||
kafka = ["windmill-api/kafka"]
|
||||
nats = ["windmill-api/nats"]
|
||||
otel = ["windmill-common/otel", "windmill-worker/otel"]
|
||||
@@ -116,6 +116,7 @@ deno_core = { workspace = true, optional = true }
|
||||
object_store = { workspace = true, optional = true }
|
||||
quote.workspace = true
|
||||
memchr.workspace = true
|
||||
v8 = { workspace = true, optional = true }
|
||||
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
@@ -133,6 +134,7 @@ windmill-api-client.workspace = true
|
||||
deno_core = { workspace = true, features = ["include_js_files_for_snapshotting", "unsafe_use_unprotected_platform"] }
|
||||
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
windmill-api = { path = "./windmill-api", default-features = false }
|
||||
windmill-queue = { path = "./windmill-queue" }
|
||||
@@ -157,6 +159,7 @@ windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
|
||||
windmill-parser-php = { path = "./parsers/windmill-parser-php" }
|
||||
windmill-api-client = { path = "./windmill-api-client" }
|
||||
|
||||
v8 = "=130.0.7" # Exact version
|
||||
memchr = "2.7.4"
|
||||
axum = { version = "^0.7", features = ["multipart"] }
|
||||
headers = "^0"
|
||||
@@ -188,6 +191,8 @@ rand = "^0"
|
||||
rand_core = { version = "^0", features = ["std"] }
|
||||
magic-crypt = "^3"
|
||||
git-version = "^0"
|
||||
malachite = "=0.4.18"
|
||||
malachite-bigint = "=0.2.0"
|
||||
rustpython-parser = "^0"
|
||||
php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" }
|
||||
cron = "^0"
|
||||
|
||||
@@ -1 +1 @@
|
||||
35402ea9fc459245d8b734b50ed98923e3061514
|
||||
7cc103e5827b1ec91180a5939a9c022a2a1856c6
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE cloud_workspace_settings DROP COLUMN last_warning_sent;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE cloud_workspace_settings ADD COLUMN last_warning_sent TIMESTAMP NULL;
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["oracledb"]'::jsonb) where name = 'worker__native' and config @> '{"worker_tags": ["nativets", "postgresql", "mysql", "graphql", "snowflake", "bigquery", "mssql"]}'::jsonb AND NOT config->'worker_tags' @> '"oracledb"'::jsonb;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE websocket_trigger DROP COLUMN can_return_message;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE websocket_trigger ADD COLUMN can_return_message BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -0,0 +1 @@
|
||||
DELETE FROM global_settings WHERE name = 'otel';
|
||||
@@ -0,0 +1 @@
|
||||
INSERT INTO global_settings (name, value) VALUES ('otel', '{}');
|
||||
@@ -18,6 +18,8 @@ regex.workspace = true
|
||||
windmill-parser.workspace = true
|
||||
windmill-common.workspace = true
|
||||
rustpython-parser.workspace = true
|
||||
malachite.workspace = true
|
||||
malachite-bigint.workspace = true
|
||||
phf.workspace = true
|
||||
itertools.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -37,6 +37,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
|
||||
"smb" => "pysmb",
|
||||
"PIL" => "Pillow",
|
||||
"googleapiclient" => "google-api-python-client",
|
||||
"googlecloudbigquery" => "google-cloud-bigquery",
|
||||
"dateutil" => "python-dateutil",
|
||||
"mailparser" => "mail-parser",
|
||||
"mailparser-reply" => "mail-parser-reply",
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1737885589,
|
||||
"narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1736320768,
|
||||
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1738117527,
|
||||
"narHash": "sha256-GFviGfaezjGLFUlxdv3zyC7rSZvTXqwcG/YsF6MDkOw=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "6a3dc6ce4132bd57359214d986db376f2333c14d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
rust-overlay,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [(import rust-overlay)];
|
||||
};
|
||||
rust = pkgs.rust-bin.nightly.latest.default.override {
|
||||
extensions = [
|
||||
"rust-src"
|
||||
];
|
||||
targets = ["wasm32-unknown-unknown"];
|
||||
};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
rust
|
||||
nodejs
|
||||
wasm-pack
|
||||
sccache
|
||||
];
|
||||
RUSTC_WRAPPER = "${pkgs.sccache}/bin/sccache";
|
||||
CARGO_PATH = "${rust}/bin/cargo";
|
||||
};
|
||||
});
|
||||
}
|
||||
+34
-4
@@ -1506,9 +1506,20 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
|
||||
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
|
||||
if *RESTART_ZOMBIE_JOBS {
|
||||
let restarted = sqlx::query!(
|
||||
"UPDATE queue SET running = false, started_at = null
|
||||
WHERE last_ping < now() - ($1 || ' seconds')::interval
|
||||
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false RETURNING id, workspace_id, last_ping",
|
||||
"WITH zombie_jobs AS (
|
||||
UPDATE queue SET running = false, started_at = null
|
||||
WHERE last_ping < now() - ($1 || ' seconds')::interval
|
||||
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false
|
||||
RETURNING id, workspace_id, last_ping
|
||||
),
|
||||
update_concurrency AS (
|
||||
UPDATE concurrency_counter cc
|
||||
SET job_uuids = job_uuids - zj.id::text
|
||||
FROM zombie_jobs zj
|
||||
INNER JOIN concurrency_key ck ON ck.job_id = zj.id
|
||||
WHERE cc.concurrency_id = ck.key
|
||||
)
|
||||
SELECT id, workspace_id, last_ping FROM zombie_jobs",
|
||||
*ZOMBIE_JOB_TIMEOUT,
|
||||
)
|
||||
.fetch_all(db)
|
||||
@@ -1652,12 +1663,31 @@ async fn handle_zombie_flows(db: &DB) -> error::Result<()> {
|
||||
tracing::error!(error_message);
|
||||
report_critical_error(error_message, db.clone(), Some(&flow.workspace_id), None).await;
|
||||
// if the flow hasn't started and is a zombie, we can simply restart it
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let concurrency_key =
|
||||
sqlx::query_scalar!("SELECT key FROM concurrency_key WHERE job_id = $1", flow.id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(key) = concurrency_key {
|
||||
sqlx::query!(
|
||||
"UPDATE concurrency_counter SET job_uuids = job_uuids - $2 WHERE concurrency_id = $1",
|
||||
key,
|
||||
flow.id.hyphenated().to_string()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE queue SET running = false, started_at = null WHERE id = $1 AND canceled = false",
|
||||
flow.id
|
||||
)
|
||||
.execute(db)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
} else {
|
||||
let id = flow.id.clone();
|
||||
let last_ping = flow.last_ping.clone();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.452.1
|
||||
version: 1.455.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -7994,8 +7994,11 @@ paths:
|
||||
type: string
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
can_return_message:
|
||||
type: boolean
|
||||
required:
|
||||
- url
|
||||
- can_return_message
|
||||
responses:
|
||||
"200":
|
||||
description: successfuly connected to websocket
|
||||
@@ -9855,6 +9858,25 @@ paths:
|
||||
$ref: "#/components/schemas/Capture"
|
||||
|
||||
/w/{workspace}/capture/{id}:
|
||||
get:
|
||||
summary: get a capture
|
||||
operationId: getCapture
|
||||
tags:
|
||||
- capture
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: capture
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Capture"
|
||||
delete:
|
||||
summary: delete a capture
|
||||
operationId: deleteCapture
|
||||
@@ -10123,6 +10145,8 @@ paths:
|
||||
properties:
|
||||
connection_settings_str:
|
||||
type: string
|
||||
azure_container_path:
|
||||
type: string
|
||||
required:
|
||||
- connection_settings_str
|
||||
|
||||
@@ -13151,6 +13175,8 @@ components:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
can_return_message:
|
||||
type: boolean
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -13164,6 +13190,7 @@ components:
|
||||
- workspace_id
|
||||
- enabled
|
||||
- filters
|
||||
- can_return_message
|
||||
|
||||
NewWebsocketTrigger:
|
||||
type: object
|
||||
@@ -13195,6 +13222,8 @@ components:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
can_return_message:
|
||||
type: boolean
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -13202,6 +13231,7 @@ components:
|
||||
- url
|
||||
- is_flow
|
||||
- filters
|
||||
- can_return_message
|
||||
|
||||
EditWebsocketTrigger:
|
||||
type: object
|
||||
@@ -13231,6 +13261,8 @@ components:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
can_return_message:
|
||||
type: boolean
|
||||
|
||||
required:
|
||||
- path
|
||||
@@ -13238,7 +13270,7 @@ components:
|
||||
- url
|
||||
- is_flow
|
||||
- filters
|
||||
|
||||
- can_return_message
|
||||
WebsocketTriggerInitialMessage:
|
||||
anyOf:
|
||||
- type: object
|
||||
|
||||
@@ -56,6 +56,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/get_configs/:runnable_kind/*path", get(get_configs))
|
||||
.route("/list/:runnable_kind/*path", get(list_captures))
|
||||
.route("/:id", delete(delete_capture))
|
||||
.route("/:id", get(get_capture))
|
||||
}
|
||||
|
||||
pub fn workspaced_unauthed_service() -> Router {
|
||||
@@ -280,7 +281,7 @@ async fn list_captures(
|
||||
|
||||
let captures = sqlx::query_as!(
|
||||
Capture,
|
||||
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload: _", trigger_extra as "trigger_extra: _"
|
||||
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '"WINDMILL_TOO_BIG"'::jsonb END as "payload!: _", trigger_extra as "trigger_extra: _"
|
||||
FROM capture
|
||||
WHERE workspace_id = $1
|
||||
AND path = $2 AND is_flow = $3
|
||||
@@ -303,6 +304,24 @@ async fn list_captures(
|
||||
Ok(Json(captures))
|
||||
}
|
||||
|
||||
async fn get_capture(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, id)): Path<(String, i64)>,
|
||||
) -> JsonResult<Capture> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let capture = sqlx::query_as!(
|
||||
Capture,
|
||||
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload!: _", trigger_extra as "trigger_extra: _" FROM capture WHERE id = $1 AND workspace_id = $2"#,
|
||||
id,
|
||||
&w_id,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(capture))
|
||||
}
|
||||
|
||||
async fn delete_capture(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -1968,20 +1968,34 @@ async fn resume_suspended_job_internal(
|
||||
resume_immediately_if_relevant(parent_flow_info, job_id, &mut tx).await?;
|
||||
}
|
||||
|
||||
let approver = approver.unwrap_or_else(|| "anonymous".to_string());
|
||||
|
||||
let audit_author = match authed {
|
||||
Some(authed) => (&authed).into(),
|
||||
None => {
|
||||
let approver = approver.unwrap_or_else(|| "anonymous".to_string());
|
||||
AuditAuthor { email: approver.clone(), username: approver, username_override: None }
|
||||
}
|
||||
None => AuditAuthor {
|
||||
email: approver.clone(),
|
||||
username: approver.clone(),
|
||||
username_override: None,
|
||||
},
|
||||
};
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&audit_author,
|
||||
"jobs.approved",
|
||||
"jobs.suspend_resume",
|
||||
ActionKind::Update,
|
||||
&w_id,
|
||||
Some(&serde_json::json!({"approved": approved, "job_id": job_id}).to_string()),
|
||||
Some(
|
||||
&serde_json::json!({
|
||||
"approved": approved,
|
||||
"job_id": job_id,
|
||||
"details": if approved {
|
||||
format!("Approved by {}", &approver)
|
||||
} else {
|
||||
format!("Cancelled by {}", &approver)
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
@@ -3527,13 +3541,14 @@ pub struct WindmillCompositeResult {
|
||||
windmill_headers: Option<HashMap<String, String>>,
|
||||
result: Option<Box<RawValue>>,
|
||||
}
|
||||
pub async fn run_wait_result(
|
||||
|
||||
pub async fn run_wait_result_internal(
|
||||
db: &DB,
|
||||
uuid: Uuid,
|
||||
w_id: String,
|
||||
node_id_for_empty_return: Option<String>,
|
||||
username: &str,
|
||||
) -> error::Result<Response> {
|
||||
) -> error::Result<(Box<RawValue>, bool)> {
|
||||
let mut result = None;
|
||||
let mut success = false;
|
||||
let timeout = TIMEOUT_WAIT_RESULT.read().await.clone().unwrap_or(600);
|
||||
@@ -3605,104 +3620,113 @@ pub async fn run_wait_result(
|
||||
};
|
||||
tokio::time::sleep(core::time::Duration::from_millis(delay)).await;
|
||||
}
|
||||
|
||||
if let Some(result) = result {
|
||||
g.done = true;
|
||||
|
||||
let composite_result = serde_json::from_str::<WindmillCompositeResult>(result.get());
|
||||
match composite_result {
|
||||
Ok(WindmillCompositeResult {
|
||||
windmill_status_code,
|
||||
windmill_content_type,
|
||||
windmill_headers,
|
||||
result: result_value,
|
||||
}) => {
|
||||
if windmill_content_type.is_none()
|
||||
&& windmill_status_code.is_none()
|
||||
&& windmill_headers.is_none()
|
||||
{
|
||||
return Ok((
|
||||
if success {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
},
|
||||
Json(result),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let status_code_or_default = windmill_status_code
|
||||
.map(|val| match StatusCode::from_u16(val) {
|
||||
Ok(sc) => Ok(sc),
|
||||
Err(_) => Err(Error::ExecutionErr("Invalid status code".to_string())),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
if !success {
|
||||
Ok(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
} else if result_value.is_some() {
|
||||
Ok(StatusCode::OK)
|
||||
} else {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
if let Some(windmill_headers) = windmill_headers {
|
||||
for (k, v) in windmill_headers {
|
||||
let k = HeaderName::from_str(k.as_str()).map_err(|err| {
|
||||
Error::InternalErr(format!("Invalid header name {k}: {err}"))
|
||||
})?;
|
||||
let v = HeaderValue::from_str(v.as_str()).map_err(|err| {
|
||||
Error::InternalErr(format!("Invalid header value {v}: {err}"))
|
||||
})?;
|
||||
headers.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(content_type) = windmill_content_type {
|
||||
let serialized_json_result = result_value
|
||||
.map(|val| val.get().to_owned())
|
||||
.unwrap_or_else(String::new);
|
||||
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
|
||||
// it falls back to the original serialized JSON if it doesn't work.
|
||||
let serialized_result =
|
||||
serde_json::from_str::<String>(serialized_json_result.as_str())
|
||||
.ok()
|
||||
.unwrap_or(serialized_json_result);
|
||||
headers.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(content_type.as_str()).map_err(|err| {
|
||||
Error::InternalErr(format!(
|
||||
"Invalid content type {content_type}: {err}"
|
||||
))
|
||||
})?,
|
||||
);
|
||||
return Ok((status_code_or_default, headers, serialized_result).into_response());
|
||||
}
|
||||
if let Some(result_value) = result_value {
|
||||
return Ok(
|
||||
(status_code_or_default, headers, Json(result_value)).into_response()
|
||||
);
|
||||
} else {
|
||||
Ok((status_code_or_default, headers).into_response())
|
||||
}
|
||||
}
|
||||
_ => Ok((
|
||||
if success {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
},
|
||||
Json(result),
|
||||
)
|
||||
.into_response()),
|
||||
}
|
||||
Ok((result, success))
|
||||
} else {
|
||||
Err(Error::ExecutionErr(format!("timeout after {}s", timeout)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_wait_result(
|
||||
db: &DB,
|
||||
uuid: Uuid,
|
||||
w_id: String,
|
||||
node_id_for_empty_return: Option<String>,
|
||||
username: &str,
|
||||
) -> error::Result<Response> {
|
||||
let (result, success) =
|
||||
run_wait_result_internal(db, uuid, w_id, node_id_for_empty_return, username).await?;
|
||||
|
||||
let composite_result = serde_json::from_str::<WindmillCompositeResult>(result.get());
|
||||
match composite_result {
|
||||
Ok(WindmillCompositeResult {
|
||||
windmill_status_code,
|
||||
windmill_content_type,
|
||||
windmill_headers,
|
||||
result: result_value,
|
||||
}) => {
|
||||
if windmill_content_type.is_none()
|
||||
&& windmill_status_code.is_none()
|
||||
&& windmill_headers.is_none()
|
||||
{
|
||||
return Ok((
|
||||
if success {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
},
|
||||
Json(result),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let status_code_or_default = windmill_status_code
|
||||
.map(|val| match StatusCode::from_u16(val) {
|
||||
Ok(sc) => Ok(sc),
|
||||
Err(_) => Err(Error::ExecutionErr("Invalid status code".to_string())),
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
if !success {
|
||||
Ok(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
} else if result_value.is_some() {
|
||||
Ok(StatusCode::OK)
|
||||
} else {
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
if let Some(windmill_headers) = windmill_headers {
|
||||
for (k, v) in windmill_headers {
|
||||
let k = HeaderName::from_str(k.as_str()).map_err(|err| {
|
||||
Error::InternalErr(format!("Invalid header name {k}: {err}"))
|
||||
})?;
|
||||
let v = HeaderValue::from_str(v.as_str()).map_err(|err| {
|
||||
Error::InternalErr(format!("Invalid header value {v}: {err}"))
|
||||
})?;
|
||||
headers.insert(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(content_type) = windmill_content_type {
|
||||
let serialized_json_result = result_value
|
||||
.map(|val| val.get().to_owned())
|
||||
.unwrap_or_else(String::new);
|
||||
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
|
||||
// it falls back to the original serialized JSON if it doesn't work.
|
||||
let serialized_result =
|
||||
serde_json::from_str::<String>(serialized_json_result.as_str())
|
||||
.ok()
|
||||
.unwrap_or(serialized_json_result);
|
||||
headers.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
HeaderValue::from_str(content_type.as_str()).map_err(|err| {
|
||||
Error::InternalErr(format!("Invalid content type {content_type}: {err}"))
|
||||
})?,
|
||||
);
|
||||
return Ok((status_code_or_default, headers, serialized_result).into_response());
|
||||
}
|
||||
if let Some(result_value) = result_value {
|
||||
return Ok((status_code_or_default, headers, Json(result_value)).into_response());
|
||||
} else {
|
||||
Ok((status_code_or_default, headers).into_response())
|
||||
}
|
||||
}
|
||||
_ => Ok((
|
||||
if success {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
},
|
||||
Json(result),
|
||||
)
|
||||
.into_response()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_job_metadata_after_use(db: &DB, job_uuid: Uuid) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
"UPDATE completed_job
|
||||
|
||||
@@ -23,7 +23,7 @@ use windmill_common::{
|
||||
error::{self, Error},
|
||||
jobs::JobKind,
|
||||
scripts::ScriptHash,
|
||||
variables::{build_crypt, decrypt_value_with_mc},
|
||||
variables::{build_crypt, decrypt},
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -853,7 +853,7 @@ async fn get_slack_token(db: &DB, slack_resource_path: &str, w_id: &str) -> anyh
|
||||
|
||||
if slack_token.is_secret {
|
||||
let mc = build_crypt(&db, w_id).await?;
|
||||
let bot_token = decrypt_value_with_mc(slack_token.value, mc).await?;
|
||||
let bot_token = decrypt(&mc, slack_token.value)?;
|
||||
Ok(bot_token)
|
||||
} else {
|
||||
Ok(slack_token.value)
|
||||
|
||||
@@ -1718,6 +1718,7 @@ pub async fn create_session_token<'c>(
|
||||
if COOKIE_DOMAIN.is_some() {
|
||||
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap());
|
||||
}
|
||||
|
||||
let mut expire: OffsetDateTime = time::OffsetDateTime::now_utc();
|
||||
expire += time::Duration::days(3);
|
||||
cookie.set_expires(expire);
|
||||
|
||||
@@ -33,7 +33,9 @@ use windmill_queue::PushArgsOwned;
|
||||
use crate::{
|
||||
capture::{insert_capture_payload, TriggerKind, WebsocketTriggerConfig},
|
||||
db::{ApiAuthed, DB},
|
||||
jobs::{run_flow_by_path_inner, run_script_by_path_inner, RunJobQuery},
|
||||
jobs::{
|
||||
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_internal, RunJobQuery,
|
||||
},
|
||||
users::fetch_api_authed,
|
||||
};
|
||||
|
||||
@@ -61,6 +63,7 @@ struct NewWebsocketTrigger {
|
||||
filters: Vec<Box<RawValue>>,
|
||||
initial_messages: Option<Vec<Box<RawValue>>>,
|
||||
url_runnable_args: Option<Box<RawValue>>,
|
||||
can_return_message: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -101,6 +104,7 @@ pub struct WebsocketTrigger {
|
||||
filters: Vec<SqlxJson<Box<RawValue>>>,
|
||||
initial_messages: Option<Vec<SqlxJson<Box<RawValue>>>>,
|
||||
url_runnable_args: Option<SqlxJson<Box<RawValue>>>,
|
||||
can_return_message: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -112,6 +116,7 @@ struct EditWebsocketTrigger {
|
||||
filters: Vec<Box<RawValue>>,
|
||||
initial_messages: Option<Vec<Box<RawValue>>>,
|
||||
url_runnable_args: Option<Box<RawValue>>,
|
||||
can_return_message: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -189,7 +194,7 @@ async fn create_websocket_trigger(
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
if *CLOUD_HOSTED {
|
||||
return Err(error::Error::BadRequest(
|
||||
"Websocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
|
||||
"WebSocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -203,7 +208,7 @@ async fn create_websocket_trigger(
|
||||
.map(SqlxJson)
|
||||
.collect_vec();
|
||||
sqlx::query_as::<_, WebsocketTrigger>(
|
||||
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, initial_messages, url_runnable_args, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now()) RETURNING *",
|
||||
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, initial_messages, url_runnable_args, edited_by, can_return_message, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now()) RETURNING *",
|
||||
)
|
||||
.bind(&w_id)
|
||||
.bind(&ct.path)
|
||||
@@ -215,6 +220,7 @@ async fn create_websocket_trigger(
|
||||
.bind(initial_messages.as_slice())
|
||||
.bind(ct.url_runnable_args.map(SqlxJson))
|
||||
.bind(&authed.username)
|
||||
.bind(ct.can_return_message)
|
||||
.bind(&authed.email)
|
||||
.fetch_one(&mut *tx).await?;
|
||||
|
||||
@@ -253,8 +259,8 @@ async fn update_websocket_trigger(
|
||||
|
||||
// important to update server_id to NULL to stop current websocket listener
|
||||
sqlx::query!(
|
||||
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, error = NULL
|
||||
WHERE workspace_id = $10 AND path = $11",
|
||||
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL
|
||||
WHERE workspace_id = $11 AND path = $12",
|
||||
ct.url,
|
||||
ct.script_path,
|
||||
ct.path,
|
||||
@@ -264,6 +270,7 @@ async fn update_websocket_trigger(
|
||||
ct.url_runnable_args.map(SqlxJson) as Option<SqlxJson<Box<RawValue>>>,
|
||||
&authed.username,
|
||||
&authed.email,
|
||||
ct.can_return_message,
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
@@ -310,7 +317,7 @@ pub async fn set_enabled(
|
||||
w_id,
|
||||
).fetch_optional(&mut *tx).await?;
|
||||
|
||||
not_found_if_none(one_o.flatten(), "Websocket trigger", path)?;
|
||||
not_found_if_none(one_o.flatten(), "WebSocket trigger", path)?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
@@ -326,7 +333,7 @@ pub async fn set_enabled(
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!(
|
||||
"succesfully updated websocket trigger at path {} to status {}",
|
||||
"succesfully updated WebSocket trigger at path {} to status {}",
|
||||
path, payload.enabled
|
||||
))
|
||||
}
|
||||
@@ -359,7 +366,7 @@ async fn delete_websocket_trigger(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(format!("Websocket trigger {path} deleted"))
|
||||
Ok(format!("WebSocket trigger {path} deleted"))
|
||||
}
|
||||
|
||||
async fn exists_websocket_trigger(
|
||||
@@ -409,7 +416,7 @@ async fn test_websocket_connection(
|
||||
)
|
||||
} else {
|
||||
return Err(error::Error::BadConfig(format!(
|
||||
"Invalid websocket runnable path: {}",
|
||||
"Invalid WebSocket runnable path: {}",
|
||||
url
|
||||
)));
|
||||
}
|
||||
@@ -419,7 +426,7 @@ async fn test_websocket_connection(
|
||||
|
||||
connect_async(connect_url.as_ref()).await.map_err(|err| {
|
||||
error::Error::BadConfig(format!(
|
||||
"Error connecting to websocket: {}",
|
||||
"Error connecting to WebSocket: {}",
|
||||
err.to_string()
|
||||
))
|
||||
})?;
|
||||
@@ -430,7 +437,7 @@ async fn test_websocket_connection(
|
||||
tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
error::Error::BadConfig(format!("Timeout connecting to websocket after 30 seconds"))
|
||||
error::Error::BadConfig(format!("Timeout connecting to WebSocket after 30 seconds"))
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
@@ -455,7 +462,7 @@ async fn listen_to_unlistened_websockets(
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error fetching websocket triggers: {:?}", err);
|
||||
tracing::error!("Error fetching WebSocket triggers: {:?}", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -473,7 +480,7 @@ async fn listen_to_unlistened_websockets(
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error fetching capture websocket triggers: {:?}", err);
|
||||
tracing::error!("Error fetching capture WebSocket triggers: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,16 +568,9 @@ where
|
||||
deserializer.deserialize_map(SupersetVisitor { key, value_to_check })
|
||||
}
|
||||
|
||||
async fn wait_runnable_result(
|
||||
path: String,
|
||||
is_flow: bool,
|
||||
fn raw_value_to_args_hashmap(
|
||||
args: Option<&Box<RawValue>>,
|
||||
authed: ApiAuthed,
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
) -> error::Result<String> {
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
) -> error::Result<HashMap<String, Box<RawValue>>> {
|
||||
let args = if let Some(args) = args {
|
||||
serde_json::from_str::<Option<HashMap<String, Box<RawValue>>>>(args.get())
|
||||
.map_err(|e| error::Error::BadRequest(format!("invalid json: {}", e)))?
|
||||
@@ -578,84 +578,89 @@ async fn wait_runnable_result(
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
let (_, job_id) = if is_flow {
|
||||
run_flow_by_path_inner(
|
||||
async fn wait_runnable_result(
|
||||
path: String,
|
||||
is_flow: bool,
|
||||
args: PushArgsOwned,
|
||||
authed: ApiAuthed,
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
) -> error::Result<String> {
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
let username = authed.display_username().to_owned();
|
||||
|
||||
let (job_id, early_return) = if is_flow {
|
||||
let (_, job_id) = run_flow_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
workspace_id.to_string(),
|
||||
StripPath(path.clone()),
|
||||
RunJobQuery::default(),
|
||||
PushArgsOwned { args, extra: None },
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let early_return = sqlx::query_scalar!(
|
||||
r#"SELECT flow_version.value->>'early_return' as early_return
|
||||
FROM flow
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 and flow.workspace_id = $2"#,
|
||||
path,
|
||||
workspace_id,
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
(job_id, early_return)
|
||||
} else {
|
||||
run_script_by_path_inner(
|
||||
let (_, job_id) = run_script_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
workspace_id.to_string(),
|
||||
StripPath(path.clone()),
|
||||
RunJobQuery::default(),
|
||||
PushArgsOwned { args, extra: None },
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
|
||||
(job_id, None)
|
||||
};
|
||||
|
||||
let start_time = tokio::time::Instant::now();
|
||||
|
||||
loop {
|
||||
if start_time.elapsed() > tokio::time::Duration::from_secs(300) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Timed out after 5m waiting for {} {} to complete",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct RawResult {
|
||||
result: Option<SqlxJson<Box<RawValue>>>,
|
||||
success: bool,
|
||||
}
|
||||
|
||||
let result = sqlx::query_as::<_, RawResult>(
|
||||
"SELECT result, success FROM completed_job WHERE id = $1 AND workspace_id = $2",
|
||||
let (result, success) = run_wait_result_internal(
|
||||
db,
|
||||
Uuid::parse_str(&job_id).unwrap(),
|
||||
workspace_id.to_string(),
|
||||
early_return,
|
||||
&username,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Error fetching job result for {} {}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path
|
||||
)
|
||||
.bind(Uuid::parse_str(&job_id).unwrap())
|
||||
.bind(workspace_id)
|
||||
.fetch_optional(db)
|
||||
.await;
|
||||
})?;
|
||||
|
||||
match result {
|
||||
Ok(Some(r)) => {
|
||||
if !r.success {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} {path} failed: {:?}",
|
||||
if is_flow { "Flow" } else { "Script" },
|
||||
r.result
|
||||
)
|
||||
.into());
|
||||
} else {
|
||||
return Ok(r.result.map(|r| r.get().to_owned()).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// not yet done, wait for 5s and check again
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Error fetching job result for {} {path}: {err}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
Err(anyhow::anyhow!(
|
||||
"{} {path} failed: {:?}",
|
||||
if is_flow { "Flow" } else { "Script" },
|
||||
result
|
||||
)
|
||||
.into())
|
||||
} else {
|
||||
Ok(result.get().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -677,23 +682,30 @@ async fn get_url_from_runnable(
|
||||
workspace_id: &str,
|
||||
) -> error::Result<String> {
|
||||
tracing::info!(
|
||||
"Running {} {} to get websocket URL",
|
||||
"Running {} {} to get WebSocket URL",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path
|
||||
);
|
||||
|
||||
let result =
|
||||
wait_runnable_result(path.to_string(), is_flow, args, authed, db, workspace_id).await?;
|
||||
let args = raw_value_to_args_hashmap(args)?;
|
||||
|
||||
if result.starts_with("\"") && result.ends_with("\"") {
|
||||
Ok(result[1..result.len() - 1].to_string())
|
||||
} else {
|
||||
Err(error::Error::BadConfig(format!(
|
||||
let result = wait_runnable_result(
|
||||
path.to_string(),
|
||||
is_flow,
|
||||
PushArgsOwned { args, extra: None },
|
||||
authed,
|
||||
db,
|
||||
workspace_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
serde_json::from_str::<String>(result.as_str()).map_err(|_| {
|
||||
error::Error::BadConfig(format!(
|
||||
"{} {} did not return a string",
|
||||
if is_flow { "Flow" } else { "Script" },
|
||||
path
|
||||
)))
|
||||
}
|
||||
path,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
impl WebsocketTrigger {
|
||||
@@ -712,11 +724,11 @@ impl WebsocketTrigger {
|
||||
if has_lock.flatten().unwrap_or(false) {
|
||||
tokio::spawn(listen_to_websocket(WebsocketEnum::Trigger(self), db, killpill_rx));
|
||||
} else {
|
||||
tracing::info!("Websocket {} already being listened to", self.url);
|
||||
tracing::info!("WebSocket {} already being listened to", self.url);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error acquiring lock for websocket {}: {:?}", self.path, err);
|
||||
tracing::error!("Error acquiring lock for WebSocket {}: {:?}", self.path, err);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -737,12 +749,12 @@ impl WebsocketTrigger {
|
||||
self.workspace_id,
|
||||
self.path,
|
||||
).execute(db).await.ok();
|
||||
tracing::info!("Websocket {} changed, disabled, or deleted, stopping...", self.url);
|
||||
tracing::info!("WebSocket {} changed, disabled, or deleted, stopping...", self.url);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error updating ping of websocket {}: {:?}", self.url, err);
|
||||
tracing::warn!("Error updating ping of WebSocket {}: {:?}", self.url, err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -758,11 +770,11 @@ impl WebsocketTrigger {
|
||||
)
|
||||
.execute(db).await {
|
||||
Ok(_) => {
|
||||
report_critical_error(format!("Disabling websocket {} because of error: {}", self.url, error), db.clone(), Some(&self.workspace_id), None).await;
|
||||
report_critical_error(format!("Disabling WebSocket {} because of error: {}", self.url, error), db.clone(), Some(&self.workspace_id), None).await;
|
||||
},
|
||||
Err(disable_err) => {
|
||||
report_critical_error(
|
||||
format!("Could not disable websocket {} with err {}, disabling because of error {}", self.path, disable_err, error),
|
||||
format!("Could not disable WebSocket {} with err {}, disabling because of error {}", self.path, disable_err, error),
|
||||
db.clone(),
|
||||
Some(&self.workspace_id),
|
||||
None,
|
||||
@@ -790,7 +802,7 @@ impl WebsocketTrigger {
|
||||
|
||||
async fn send_initial_messages(
|
||||
&self,
|
||||
mut writer: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
|
||||
writer: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
|
||||
db: &DB,
|
||||
) -> error::Result<()> {
|
||||
let initial_messages: Vec<InitialMessage> = self
|
||||
@@ -810,7 +822,7 @@ impl WebsocketTrigger {
|
||||
msg
|
||||
};
|
||||
tracing::info!(
|
||||
"Sending raw message initial message to websocket {}: {}",
|
||||
"Sending raw message initial message to WebSocket {}: {}",
|
||||
self.url,
|
||||
msg
|
||||
);
|
||||
@@ -822,16 +834,18 @@ impl WebsocketTrigger {
|
||||
}
|
||||
InitialMessage::RunnableResult { path, is_flow, args } => {
|
||||
tracing::info!(
|
||||
"Running {} {} for initial message to websocket {}",
|
||||
"Running {} {} for initial message to WebSocket {}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path,
|
||||
self.url,
|
||||
);
|
||||
|
||||
let args = raw_value_to_args_hashmap(Some(&args))?;
|
||||
|
||||
let result = wait_runnable_result(
|
||||
path.clone(),
|
||||
is_flow,
|
||||
Some(&args),
|
||||
PushArgsOwned { args, extra: None },
|
||||
self.fetch_authed(db).await?,
|
||||
db,
|
||||
&self.workspace_id,
|
||||
@@ -839,17 +853,15 @@ impl WebsocketTrigger {
|
||||
.await?;
|
||||
|
||||
tracing::info!(
|
||||
"Sending {} {} result to websocket {}",
|
||||
"Sending {} {} result to WebSocket {}",
|
||||
if is_flow { "flow" } else { "script" },
|
||||
path,
|
||||
self.url
|
||||
);
|
||||
|
||||
let result = if result.starts_with("\"") && result.ends_with("\"") {
|
||||
result[1..result.len() - 1].to_string()
|
||||
} else {
|
||||
result
|
||||
};
|
||||
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
|
||||
// it falls back to the original serialized JSON if it doesn't work.
|
||||
let result = serde_json::from_str::<String>(result.as_str()).unwrap_or(result);
|
||||
|
||||
writer
|
||||
.send(tokio_tungstenite::tungstenite::Message::Text(result))
|
||||
@@ -869,11 +881,16 @@ impl WebsocketTrigger {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle(&self, db: &DB, args: PushArgsOwned) -> () {
|
||||
if let Err(err) = run_job(db, self, args).await {
|
||||
async fn handle(
|
||||
&self,
|
||||
db: &DB,
|
||||
args: PushArgsOwned,
|
||||
return_message_channels: Option<ReturnMessageChannels>,
|
||||
) -> () {
|
||||
if let Err(err) = run_job(db, self, args, return_message_channels).await {
|
||||
report_critical_error(
|
||||
format!(
|
||||
"Failed to trigger job from websocket {}: {:?}",
|
||||
"Failed to trigger job from WebSocket {}: {:?}",
|
||||
self.url, err
|
||||
),
|
||||
db.clone(),
|
||||
@@ -923,11 +940,11 @@ impl CaptureConfigForWebsocket {
|
||||
if has_lock.flatten().unwrap_or(false) {
|
||||
tokio::spawn(listen_to_websocket(WebsocketEnum::Capture(self), db, killpill_rx));
|
||||
} else {
|
||||
tracing::info!("Websocket {} already being listened to", self.trigger_config.url);
|
||||
tracing::info!("WebSocket {} already being listened to", self.trigger_config.url);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error acquiring lock for capture websocket {}: {:?}", self.path, err);
|
||||
tracing::error!("Error acquiring lock for capture WebSocket {}: {:?}", self.path, err);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -950,12 +967,12 @@ impl CaptureConfigForWebsocket {
|
||||
self.path,
|
||||
self.is_flow,
|
||||
).execute(db).await.ok();
|
||||
tracing::info!("Websocket capture {} changed, disabled, or deleted, stopping...", self.trigger_config.url);
|
||||
tracing::info!("WebSocket capture {} changed, disabled, or deleted, stopping...", self.trigger_config.url);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error updating ping of capture websocket {}: {:?}", self.trigger_config.url, err);
|
||||
tracing::warn!("Error updating ping of capture WebSocket {}: {:?}", self.trigger_config.url, err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1021,7 +1038,7 @@ impl CaptureConfigForWebsocket {
|
||||
self.is_flow,
|
||||
)
|
||||
.execute(db).await {
|
||||
tracing::error!("Could not disable websocket capture {} ({}) with err {}, disabling because of error {}", self.path, self.workspace_id, err, error);
|
||||
tracing::error!("Could not disable WebSocket capture {} ({}) with err {}, disabling because of error {}", self.path, self.workspace_id, err, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,6 +1086,20 @@ impl WebsocketEnum {
|
||||
}
|
||||
}
|
||||
|
||||
struct ReturnMessageChannels {
|
||||
send_message_tx: tokio::sync::mpsc::Sender<String>,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
}
|
||||
|
||||
impl Clone for ReturnMessageChannels {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
send_message_tx: self.send_message_tx.clone(),
|
||||
killpill_rx: self.killpill_rx.resubscribe(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_to_websocket(
|
||||
ws: WebsocketEnum,
|
||||
db: DB,
|
||||
@@ -1097,7 +1128,7 @@ async fn listen_to_websocket(
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, Some(
|
||||
"Waiting on runnable to return websocket URL..."
|
||||
"Waiting on runnable to return WebSocket URL..."
|
||||
)) => {
|
||||
return;
|
||||
},
|
||||
@@ -1106,7 +1137,7 @@ async fn listen_to_websocket(
|
||||
Ok(url) => Cow::Owned(url),
|
||||
Err(err) => {
|
||||
ws.disable_with_error(&db, format!(
|
||||
"Error getting websocket URL from runnable after 5 tries: {:?}",
|
||||
"Error getting WebSocket URL from runnable after 5 tries: {:?}",
|
||||
err
|
||||
),
|
||||
)
|
||||
@@ -1116,7 +1147,7 @@ async fn listen_to_websocket(
|
||||
},
|
||||
}
|
||||
} else {
|
||||
ws.disable_with_error(&db, format!("Invalid websocket runnable path: {}", url))
|
||||
ws.disable_with_error(&db, format!("Invalid WebSocket runnable path: {}", url))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
@@ -1135,8 +1166,8 @@ async fn listen_to_websocket(
|
||||
connection = connect_async(connect_url.as_ref()) => {
|
||||
match connection {
|
||||
Ok((ws_stream, _)) => {
|
||||
tracing::info!("Connected to websocket {}", url);
|
||||
let (writer, mut reader) = ws_stream.split();
|
||||
tracing::info!("Connected to WebSocket {}", url);
|
||||
let (mut writer, mut reader) = ws_stream.split();
|
||||
|
||||
// send initial messages
|
||||
match &ws {
|
||||
@@ -1149,110 +1180,134 @@ async fn listen_to_websocket(
|
||||
_ = loop_ping(&db, &ws, Some("Sending initial messages...")) => {
|
||||
return;
|
||||
},
|
||||
result = ws_trigger.send_initial_messages(writer, &db) => {
|
||||
result = ws_trigger.send_initial_messages(&mut writer, &db) => {
|
||||
if let Err(err) = result {
|
||||
ws_trigger.disable_with_error(&db, format!("Error sending initial messages: {:?}", err)).await;
|
||||
return
|
||||
} else {
|
||||
tracing::debug!("Initial messages sent successfully to websocket {}", url);
|
||||
tracing::debug!("Initial messages sent successfully to WebSocket {}", url);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
_ => {
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
_ = loop_ping(&db, &ws, None) => {
|
||||
return;
|
||||
},
|
||||
_ = async {
|
||||
loop {
|
||||
if let Some(msg) = reader.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
match msg {
|
||||
tokio_tungstenite::tungstenite::Message::Text(text) => {
|
||||
tracing::debug!("Received text message from websocket {}: {}", url, text);
|
||||
let mut should_handle = true;
|
||||
for filter in &filters {
|
||||
match filter {
|
||||
Filter::JsonFilter(JsonFilter { key, value }) => {
|
||||
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
|
||||
should_handle = match is_value_superset(&mut deserializer, key, &value) {
|
||||
Ok(filter_match) => {
|
||||
filter_match
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error deserializing filter for websocket {}: {:?}", url, err);
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if !should_handle {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if should_handle {
|
||||
|
||||
let args = HashMap::from([("msg".to_string(), to_raw_value(&text))]);
|
||||
let extra = Some(HashMap::from([(
|
||||
"wm_trigger".to_string(),
|
||||
to_raw_value(&serde_json::json!({"kind": "websocket", "websocket": { "url": url }})),
|
||||
)]));
|
||||
|
||||
let args = PushArgsOwned { args, extra };
|
||||
match &ws {
|
||||
WebsocketEnum::Trigger(ws_trigger) => {
|
||||
ws_trigger.handle(&db, args).await;
|
||||
},
|
||||
WebsocketEnum::Capture(capture) => {
|
||||
capture.handle(&db, args).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
a @ _ => {
|
||||
tracing::debug!("Received non text-message from websocket {}: {:?}", url, a);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error reading from websocket {}: {:?}", url, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Websocket {} closed", url);
|
||||
if let None = ws.update_ping(&db, Some("Websocket closed")).await {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
let (return_message_channels, message_sender_handle) = match &ws {
|
||||
WebsocketEnum::Trigger(ws_trigger) if ws_trigger.can_return_message => {
|
||||
let (send_message_tx, mut rx) = tokio::sync::mpsc::channel::<String>(100);
|
||||
let w_id = ws_trigger.workspace_id.clone();
|
||||
let url = ws_trigger.url.clone();
|
||||
let db = db.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
if let Err(err) = writer.send(tokio_tungstenite::tungstenite::Message::Text(message)).await {
|
||||
report_critical_error(format!("Could not send runnable result to WebSocket {} because of error: {}", url, err), db.clone(), Some(&w_id), None).await;
|
||||
}
|
||||
}
|
||||
} => {
|
||||
return;
|
||||
});
|
||||
|
||||
let killpill_rx = killpill_rx.resubscribe();
|
||||
|
||||
let return_message_channels = ReturnMessageChannels {
|
||||
send_message_tx,
|
||||
killpill_rx
|
||||
};
|
||||
|
||||
(Some(return_message_channels), Some(handle))
|
||||
},
|
||||
_ => (None, None)
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv() => {},
|
||||
_ = loop_ping(&db, &ws, None) => {},
|
||||
_ = async {
|
||||
loop {
|
||||
if let Some(msg) = reader.next().await {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
match msg {
|
||||
tokio_tungstenite::tungstenite::Message::Text(text) => {
|
||||
tracing::debug!("Received text message from WebSocket {}: {}", url, text);
|
||||
let mut should_handle = true;
|
||||
for filter in &filters {
|
||||
match filter {
|
||||
Filter::JsonFilter(JsonFilter { key, value }) => {
|
||||
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
|
||||
should_handle = match is_value_superset(&mut deserializer, key, &value) {
|
||||
Ok(filter_match) => {
|
||||
filter_match
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!("Error deserializing filter for WebSocket {}: {:?}", url, err);
|
||||
false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if !should_handle {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if should_handle {
|
||||
|
||||
let args = HashMap::from([("msg".to_string(), to_raw_value(&text))]);
|
||||
let extra = Some(HashMap::from([(
|
||||
"wm_trigger".to_string(),
|
||||
to_raw_value(&serde_json::json!({"kind": "websocket", "websocket": { "url": url }})),
|
||||
)]));
|
||||
|
||||
let args = PushArgsOwned { args, extra };
|
||||
match &ws {
|
||||
WebsocketEnum::Trigger(ws_trigger) => {
|
||||
ws_trigger.handle(&db, args, return_message_channels.clone()).await;
|
||||
},
|
||||
WebsocketEnum::Capture(capture) => {
|
||||
capture.handle(&db, args).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
a @ _ => {
|
||||
tracing::debug!("Received non text-message from WebSocket {}: {:?}", url, a);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::error!("Error reading from WebSocket {}: {:?}", url, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("WebSocket {} closed", url);
|
||||
ws.update_ping(&db, Some("WebSocket closed")).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} => {}
|
||||
}
|
||||
// make sure to stop return message handler
|
||||
if let Some(message_sender_handle) = message_sender_handle {
|
||||
message_sender_handle.abort();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
|
||||
if let None = ws.update_ping(&db, Some(err.to_string().as_str())).await {
|
||||
return;
|
||||
}
|
||||
tracing::error!("Error connecting to WebSocket {}: {:?}", url, err);
|
||||
ws.update_ping(&db, Some(err.to_string().as_str())).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> anyhow::Result<()> {
|
||||
async fn run_job(
|
||||
db: &DB,
|
||||
trigger: &WebsocketTrigger,
|
||||
args: PushArgsOwned,
|
||||
return_message_channels: Option<ReturnMessageChannels>,
|
||||
) -> anyhow::Result<()> {
|
||||
let authed = fetch_api_authed(
|
||||
trigger.edited_by.clone(),
|
||||
trigger.email.clone(),
|
||||
@@ -1262,34 +1317,73 @@ async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> an
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user_db = UserDB::new(db.clone());
|
||||
if let Some(ReturnMessageChannels { send_message_tx, mut killpill_rx }) =
|
||||
return_message_channels
|
||||
{
|
||||
let db_ = db.clone();
|
||||
let url = trigger.url.clone();
|
||||
let script_path = trigger.script_path.clone();
|
||||
let is_flow = trigger.is_flow;
|
||||
let w_id = trigger.workspace_id.clone();
|
||||
let handle_response_f = async move {
|
||||
tokio::select! {
|
||||
_ = killpill_rx.recv() => {
|
||||
return;
|
||||
},
|
||||
result = wait_runnable_result(
|
||||
script_path,
|
||||
is_flow,
|
||||
args,
|
||||
authed,
|
||||
&db_,
|
||||
&w_id,
|
||||
) => {
|
||||
if let Ok(result) = result {
|
||||
// only send the result if it's not null
|
||||
if result != "null" {
|
||||
tracing::info!("Sending job result to WebSocket {}", url);
|
||||
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
|
||||
// it falls back to the original serialized JSON if it doesn't work.
|
||||
let result = serde_json::from_str::<String>(result.as_str()).unwrap_or(result);
|
||||
if let Err(err) = send_message_tx.send(result).await {
|
||||
report_critical_error(format!("Could not send runnable result to WebSocket {} because of error: {}", url, err), db_.clone(), Some(&w_id), None).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let run_query = RunJobQuery::default();
|
||||
|
||||
if trigger.is_flow {
|
||||
run_flow_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
trigger.workspace_id.clone(),
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
tokio::spawn(handle_response_f);
|
||||
} else {
|
||||
run_script_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
trigger.workspace_id.clone(),
|
||||
StripPath(trigger.script_path.to_owned()),
|
||||
run_query,
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let user_db = UserDB::new(db.clone());
|
||||
let run_query = RunJobQuery::default();
|
||||
let runnable_path = StripPath(trigger.script_path.to_owned());
|
||||
if trigger.is_flow {
|
||||
run_flow_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
trigger.workspace_id.clone(),
|
||||
runnable_path,
|
||||
run_query,
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
run_script_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
trigger.workspace_id.clone(),
|
||||
runnable_path,
|
||||
run_query,
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -529,7 +529,7 @@ pub mod script {
|
||||
lock AS \"lock: String\", \
|
||||
language AS \"language: Option<ScriptLang>\", \
|
||||
envs AS \"envs: Vec<String>\", \
|
||||
codebase AS \"codebase: String\" \
|
||||
codebase LIKE '%.tar' as use_tar \
|
||||
FROM script WHERE hash = $1 LIMIT 1",
|
||||
hash.0
|
||||
)
|
||||
@@ -543,7 +543,16 @@ pub mod script {
|
||||
meta: Some(ScriptMetadata {
|
||||
language: r.language,
|
||||
envs: r.envs,
|
||||
codebase: r.codebase,
|
||||
codebase: if let Some(use_tar) = r.use_tar {
|
||||
let sh = hash.to_string();
|
||||
if use_tar {
|
||||
Some(format!("{sh}.tar"))
|
||||
} else {
|
||||
Some(sh)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::error;
|
||||
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use magic_crypt::{MagicCrypt256, MagicCryptError, MagicCryptTrait};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::error;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SECRET_SALT: Option<String> = std::env::var("SECRET_SALT").ok();
|
||||
@@ -134,7 +133,7 @@ pub async fn get_secret_value_as_admin(
|
||||
let value = variable.value;
|
||||
if !value.is_empty() {
|
||||
let mc = build_crypt(db, w_id).await?;
|
||||
decrypt_value_with_mc(value, mc).await?
|
||||
decrypt(&mc, value)?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
@@ -145,16 +144,6 @@ pub async fn get_secret_value_as_admin(
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub async fn decrypt_value_with_mc(value: String, mc: MagicCrypt256) -> Result<String> {
|
||||
mc.decrypt_base64_to_string(value).map_err(|e| match e {
|
||||
MagicCryptError::DecryptError(_) => crate::error::Error::InternalErr(
|
||||
"Could not decrypt value. The value may have been encrypted with a different key."
|
||||
.to_string(),
|
||||
),
|
||||
_ => crate::error::Error::InternalErr(e.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
|
||||
mc.encrypt_str_to_base64(value)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,13 @@ mount {
|
||||
is_bind: true
|
||||
}
|
||||
|
||||
mount {
|
||||
src: "{GLOBAL_SITE_PACKAGES}"
|
||||
dst: "{GLOBAL_SITE_PACKAGES}"
|
||||
is_bind: true
|
||||
mandatory: false
|
||||
}
|
||||
|
||||
{SHARED_MOUNT}
|
||||
|
||||
{SHARED_DEPENDENCIES}
|
||||
|
||||
@@ -18,7 +18,7 @@ use windmill_common::jobs::ENTRYPOINT_OVERRIDE;
|
||||
use windmill_common::s3_helpers::{
|
||||
get_etag_or_empty, LargeFileStorage, ObjectStoreResource, S3Object,
|
||||
};
|
||||
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt_value_with_mc};
|
||||
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt};
|
||||
use windmill_common::worker::{
|
||||
to_raw_value, write_file, CLOUD_HOSTED, ROOT_CACHE_DIR, WORKER_CONFIG,
|
||||
};
|
||||
@@ -255,11 +255,9 @@ pub async fn transform_json_value(
|
||||
let encrypted = y.strip_prefix("$encrypted:").unwrap();
|
||||
let mc =
|
||||
build_crypt_with_key_suffix(&db, &job.workspace_id, &job.id.to_string()).await?;
|
||||
decrypt_value_with_mc(encrypted.to_string(), mc)
|
||||
.await
|
||||
.and_then(|x| {
|
||||
serde_json::from_str(&x).map_err(|e| Error::InternalErr(e.to_string()))
|
||||
})
|
||||
decrypt(&mc, encrypted.to_string()).and_then(|x| {
|
||||
serde_json::from_str(&x).map_err(|e| Error::InternalErr(e.to_string()))
|
||||
})
|
||||
|
||||
// let path = y.strip_prefix("$res:").unwrap();
|
||||
}
|
||||
|
||||
@@ -36,36 +36,38 @@ use windmill_common::{
|
||||
use windmill_common::variables::get_secret_value_as_admin;
|
||||
|
||||
use windmill_queue::{append_logs, CanceledBy};
|
||||
use std::env::var;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PYTHON_PATH: String =
|
||||
std::env::var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
|
||||
var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
|
||||
|
||||
static ref UV_PATH: String =
|
||||
std::env::var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
|
||||
var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
|
||||
|
||||
static ref PY_CONCURRENT_DOWNLOADS: usize =
|
||||
std::env::var("PY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
|
||||
var("PY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
|
||||
|
||||
static ref FLOCK_PATH: String =
|
||||
std::env::var("FLOCK_PATH").unwrap_or_else(|_| "/usr/bin/flock".to_string());
|
||||
var("FLOCK_PATH").unwrap_or_else(|_| "/usr/bin/flock".to_string());
|
||||
static ref NON_ALPHANUM_CHAR: Regex = regex::Regex::new(r"[^0-9A-Za-z=.-]").unwrap();
|
||||
|
||||
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
|
||||
static ref PIP_INDEX_CERT: Option<String> = std::env::var("PIP_INDEX_CERT").ok();
|
||||
static ref TRUSTED_HOST: Option<String> = var("PY_TRUSTED_HOST").ok().or(var("PIP_TRUSTED_HOST").ok());
|
||||
static ref INDEX_CERT: Option<String> = var("PY_INDEX_CERT").ok().or(var("PIP_INDEX_CERT").ok());
|
||||
static ref NATIVE_CERT: bool = var("PY_NATIVE_CERT").ok().or(var("UV_NATIVE_TLS").ok()).map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
pub static ref USE_SYSTEM_PYTHON: bool = std::env::var("USE_SYSTEM_PYTHON")
|
||||
pub static ref USE_SYSTEM_PYTHON: bool = var("USE_SYSTEM_PYTHON")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
pub static ref USE_PIP_COMPILE: bool = std::env::var("USE_PIP_COMPILE")
|
||||
pub static ref USE_PIP_COMPILE: bool = var("USE_PIP_COMPILE")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
pub static ref USE_PIP_INSTALL: bool = std::env::var("USE_PIP_INSTALL")
|
||||
pub static ref USE_PIP_INSTALL: bool = var("USE_PIP_INSTALL")
|
||||
.ok().map(|flag| flag == "true").unwrap_or(false);
|
||||
|
||||
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
|
||||
|
||||
static ref EPHEMERAL_TOKEN_CMD: Option<String> = std::env::var("EPHEMERAL_TOKEN_CMD").ok();
|
||||
static ref EPHEMERAL_TOKEN_CMD: Option<String> = var("EPHEMERAL_TOKEN_CMD").ok();
|
||||
}
|
||||
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
|
||||
@@ -532,10 +534,10 @@ pub async fn uv_pip_compile(
|
||||
args.extend(["--index-url", url, "--no-emit-index-url"]);
|
||||
pip_args.push(format!("--index-url {}", url));
|
||||
}
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
if let Some(host) = TRUSTED_HOST.as_ref() {
|
||||
args.extend(["--trusted-host", host]);
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
if let Some(cert_path) = INDEX_CERT.as_ref() {
|
||||
args.extend(["--cert", cert_path]);
|
||||
}
|
||||
let pip_args_str = pip_args.join(" ");
|
||||
@@ -621,12 +623,15 @@ pub async fn uv_pip_compile(
|
||||
if let Some(url) = pip_index_url.as_ref() {
|
||||
args.extend(["--index-url", url]);
|
||||
}
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
if let Some(host) = TRUSTED_HOST.as_ref() {
|
||||
args.extend(["--trusted-host", host]);
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
if let Some(cert_path) = INDEX_CERT.as_ref() {
|
||||
args.extend(["--cert", cert_path]);
|
||||
}
|
||||
if *NATIVE_CERT {
|
||||
args.extend(["--native-tls"]);
|
||||
}
|
||||
tracing::error!("uv args: {:?}", args);
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -897,6 +902,8 @@ pub async fn handle_python_job(
|
||||
tracing::debug!("Finished deps postinstall stage");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if no_uv {
|
||||
append_logs(
|
||||
&job.id,
|
||||
@@ -1045,7 +1052,27 @@ except BaseException as e:
|
||||
|
||||
let client = client.get_authed().await;
|
||||
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
|
||||
let additional_python_paths_folders = additional_python_paths.iter().join(":");
|
||||
|
||||
// Add /tmp/windmill/cache/python_xyz/global-site-packages to PYTHONPATH.
|
||||
// Usefull if certain wheels needs to be preinstalled before execution.
|
||||
let global_site_packages_path = py_version.to_cache_dir() + "/global-site-packages";
|
||||
let additional_python_paths_folders = {
|
||||
let mut paths= additional_python_paths.clone();
|
||||
if std::fs::metadata(&global_site_packages_path).is_ok() {
|
||||
// We want global_site_packages_path to be included in additonal_python_paths_folders, but
|
||||
// we don't want it to be included in global_site_packages_path.
|
||||
// The reason for this is that additional_python_paths_folders is used to fill PYTHONPATH env variable for jailed script
|
||||
// When global_site_packages_path used to place mount point of wheels to the jail config.
|
||||
// Since we handle mount of global_site_packages on our own, we don't want it to be mounted automatically.
|
||||
// We do this because existence of every wheel in cache is mandatory and if it is not there and nsjail expects it, it is a bug.
|
||||
// On the other side global_site_packages is purely optional.
|
||||
// NOTE: This behaviour can be changed in future, so verification of wheels can be offloaded from nsjail to windmill
|
||||
paths.insert(0, global_site_packages_path.clone());
|
||||
// ^^^^^^^^
|
||||
// We also want this be priorotized, that's why we insert it to the beginning
|
||||
}
|
||||
paths.iter().join(":")
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let additional_python_paths_folders = additional_python_paths_folders.replace(":", ";");
|
||||
@@ -1075,6 +1102,7 @@ mount {{
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{SHARED_MOUNT}", shared_mount)
|
||||
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
|
||||
.replace("{GLOBAL_SITE_PACKAGES}", &global_site_packages_path)
|
||||
.replace("{MAIN}", format!("{dirs}/{last}").as_str())
|
||||
.replace(
|
||||
"{ADDITIONAL_PYTHON_PATHS}",
|
||||
@@ -1572,12 +1600,15 @@ async fn spawn_uv_install(
|
||||
if let Some(url) = pip_index_url.as_ref() {
|
||||
vars.push(("INDEX_URL", url));
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
vars.push(("PIP_INDEX_CERT", cert_path));
|
||||
if let Some(cert_path) = INDEX_CERT.as_ref() {
|
||||
vars.push(("SSL_CERT_FILE", cert_path));
|
||||
}
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
if let Some(host) = TRUSTED_HOST.as_ref() {
|
||||
vars.push(("TRUSTED_HOST", host));
|
||||
}
|
||||
if *NATIVE_CERT {
|
||||
vars.push(("UV_NATIVE_TLS", "true"));
|
||||
}
|
||||
let _owner;
|
||||
if let Some(py_path) = py_path.as_ref() {
|
||||
_owner = format!(
|
||||
@@ -1674,18 +1705,26 @@ async fn spawn_uv_install(
|
||||
});
|
||||
}
|
||||
|
||||
let mut envs = vec![("PATH", PATH_ENV.as_str())];
|
||||
envs.push(("HOME", HOME_ENV.as_str()));
|
||||
|
||||
if let Some(url) = pip_index_url.as_ref() {
|
||||
command_args.extend(["--index-url", url]);
|
||||
}
|
||||
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
|
||||
command_args.extend(["--cert", cert_path]);
|
||||
}
|
||||
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
|
||||
if let Some(host) = TRUSTED_HOST.as_ref() {
|
||||
command_args.extend(["--trusted-host", &host]);
|
||||
}
|
||||
|
||||
let mut envs = vec![("PATH", PATH_ENV.as_str())];
|
||||
envs.push(("HOME", HOME_ENV.as_str()));
|
||||
if *NATIVE_CERT {
|
||||
command_args.extend(["--native-tls"]);
|
||||
}
|
||||
// TODO:
|
||||
// Track https://github.com/astral-sh/uv/issues/6715
|
||||
if let Some(cert_path) = INDEX_CERT.as_ref() {
|
||||
// Once merged --cert can be used instead
|
||||
//
|
||||
// command_args.extend(["--cert", cert_path]);
|
||||
envs.push(("SSL_CERT_FILE", cert_path));
|
||||
}
|
||||
|
||||
tracing::debug!("uv pip install command: {:?}", command_args);
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ use windmill_queue::{
|
||||
|
||||
type DB = sqlx::Pool<sqlx::Postgres>;
|
||||
|
||||
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
|
||||
use windmill_audit::ActionKind;
|
||||
use windmill_queue::{canceled_job_to_result, push};
|
||||
|
||||
// #[instrument(level = "trace", skip_all)]
|
||||
@@ -1894,6 +1896,12 @@ async fn push_next_flow_job(
|
||||
))
|
||||
&& suspend.continue_on_disapprove_timeout.unwrap_or(false);
|
||||
|
||||
let audit_author = AuditAuthor {
|
||||
username: flow_job.permissioned_as.trim_start_matches("u/").to_string(),
|
||||
email: flow_job.email.clone(),
|
||||
username_override: None,
|
||||
};
|
||||
|
||||
if can_be_resumed || disapproved_or_timeout_but_continue {
|
||||
if disapproved_or_timeout_but_continue {
|
||||
let js = if let Some(disapproved) = is_disapproved.as_ref() {
|
||||
@@ -1903,7 +1911,18 @@ async fn push_next_flow_job(
|
||||
};
|
||||
|
||||
resume_messages.push(to_raw_value(&js));
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&audit_author,
|
||||
"jobs.suspend_resume",
|
||||
ActionKind::Update,
|
||||
&flow_job.workspace_id,
|
||||
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval but can continue".to_string()}).to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE queue
|
||||
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'approvers'], $2)
|
||||
@@ -1971,6 +1990,18 @@ async fn push_next_flow_job(
|
||||
|
||||
/* cancelled or we're WaitingForEvents but we don't have enough messages (timed out) */
|
||||
} else {
|
||||
if is_disapproved.is_none() {
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&audit_author,
|
||||
"jobs.suspend_resume",
|
||||
ActionKind::Update,
|
||||
&flow_job.workspace_id,
|
||||
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval and is cancelled".to_string()}).to_string()),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
tx.commit().await?;
|
||||
|
||||
let (logs, error_name) = if let Some(disapprover) = is_disapproved {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.452.1";
|
||||
export const VERSION = "v1.455.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface SyncOptions {
|
||||
excludes?: string[];
|
||||
defaultTs?: "bun" | "deno";
|
||||
codebases?: Codebase[];
|
||||
parallel?: number;
|
||||
}
|
||||
|
||||
export interface Codebase {
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.452.1";
|
||||
export const VERSION = "1.455.1";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
+26
-23
@@ -193,11 +193,12 @@ export async function handleFile(
|
||||
bundleContent = execSync(
|
||||
codebase.customBundler + " " + path
|
||||
).toString();
|
||||
log.info("Custom bundler executed");
|
||||
log.info("Custom bundler executed for " + path);
|
||||
} else {
|
||||
const esbuild = await import("npm:esbuild");
|
||||
|
||||
log.info(`Starting building the bundle for ${path}`);
|
||||
log.info(`Started bundling ${path} ...`);
|
||||
const startTime = performance.now();
|
||||
const out = await esbuild.build({
|
||||
entryPoints: [path],
|
||||
format: "cjs",
|
||||
@@ -210,19 +211,20 @@ export async function handleFile(
|
||||
packages: "bundle",
|
||||
target: "node20.15.1",
|
||||
});
|
||||
const endTime = performance.now();
|
||||
bundleContent = out.outputFiles[0].text;
|
||||
log.info(
|
||||
"Bundle size: " + (bundleContent.length / 1024).toFixed(0) + "kB"
|
||||
`Finished bundling ${path}: ${(bundleContent.length / 1024).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)`
|
||||
);
|
||||
}
|
||||
if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
|
||||
const archiveNpm = await import("npm:@ayonli/jsext/archive");
|
||||
|
||||
log.info(
|
||||
`Using the following asset configuration: ${JSON.stringify(
|
||||
`Using the following asset configuration for ${path}: ${JSON.stringify(
|
||||
codebase.assets
|
||||
)}`
|
||||
);
|
||||
const startTime = performance.now();
|
||||
const tarball = new archiveNpm.Tarball();
|
||||
tarball.append(
|
||||
new File([bundleContent], "main.js", { type: "text/plain" })
|
||||
@@ -233,10 +235,10 @@ export async function handleFile(
|
||||
const file = new File([blob], asset.to);
|
||||
tarball.append(file);
|
||||
}
|
||||
log.info("Tarball size: " + (tarball.size / 1024).toFixed(0) + "kB");
|
||||
const endTime = performance.now();
|
||||
log.info(`Finished creating tarball for ${path}: ${(tarball.size / 1024).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)`);
|
||||
bundleContent = tarball;
|
||||
}
|
||||
log.info(`Finished building the bundle for ${path}`);
|
||||
}
|
||||
let typed =
|
||||
opts?.skipScriptsMetadata ? undefined :
|
||||
@@ -364,24 +366,23 @@ export async function handleFile(
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
colors.yellow.bold(`Creating script with a parent ${remotePath}`)
|
||||
);
|
||||
|
||||
log.info(`Updating script ${remotePath} ...`);
|
||||
const body = {
|
||||
...requestBodyCommon,
|
||||
parent_hash: remote.hash,
|
||||
};
|
||||
await createScript(bundleContent, workspaceId, body, workspace);
|
||||
const execTime = await createScript(bundleContent, workspaceId, body, workspace);
|
||||
log.info(colors.yellow.bold(`Updated script ${remotePath} (${execTime.toFixed(0)}ms)`));
|
||||
} else {
|
||||
log.info(
|
||||
colors.yellow.bold(`Creating script without parent ${remotePath}`)
|
||||
);
|
||||
|
||||
log.info(`Creating new script ${remotePath} ...`);
|
||||
const body = {
|
||||
...requestBodyCommon,
|
||||
parent_hash: undefined,
|
||||
};
|
||||
await createScript(bundleContent, workspaceId, body, workspace);
|
||||
const execTime = await createScript(bundleContent, workspaceId, body, workspace);
|
||||
log.info(colors.yellow.bold(`Created new script ${remotePath} (${execTime.toFixed(0)}ms)`));
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -416,7 +417,8 @@ async function createScript(
|
||||
workspaceId: string,
|
||||
body: NewScript,
|
||||
workspace: Workspace
|
||||
) {
|
||||
): Promise<number> {
|
||||
const start = performance.now();
|
||||
if (!bundleContent) {
|
||||
try {
|
||||
// no parent hash
|
||||
@@ -427,7 +429,7 @@ async function createScript(
|
||||
} catch (e: any) {
|
||||
throw Error(
|
||||
`Script creation for ${body.path} with parent ${body.parent_hash
|
||||
} was not successful: ${e.body ?? e.message}`
|
||||
} was not successful: ${e.body ?? e.message} `
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -447,16 +449,17 @@ async function createScript(
|
||||
"/scripts/create_snapshot";
|
||||
const req = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${workspace.token}` },
|
||||
headers: { Authorization: `Bearer ${workspace.token} ` },
|
||||
body: form,
|
||||
});
|
||||
if (req.status != 201) {
|
||||
throw Error(
|
||||
`Script snapshot creation was not successful: ${req.status} - ${req.statusText
|
||||
} - ${await req.text()}`
|
||||
} - ${await req.text()} `
|
||||
);
|
||||
}
|
||||
}
|
||||
return performance.now() - start;
|
||||
}
|
||||
|
||||
export async function findContentFile(filePath: string) {
|
||||
@@ -868,7 +871,7 @@ async function generateMetadata(
|
||||
} & SyncOptions,
|
||||
scriptPath: string | undefined
|
||||
) {
|
||||
log.info("This command only works for workspace scripts, for flows inline scripts use `wmill flow generate-locks`");
|
||||
log.info("This command only works for workspace scripts, for flows inline scripts use `wmill flow generate - locks`");
|
||||
if (scriptPath == "") {
|
||||
scriptPath = undefined;
|
||||
}
|
||||
@@ -924,7 +927,7 @@ async function generateMetadata(
|
||||
);
|
||||
if (candidate) {
|
||||
hasAny = true;
|
||||
log.info(colors.green(`+ ${candidate}`));
|
||||
log.info(colors.green(`+ ${candidate} `));
|
||||
}
|
||||
}
|
||||
if (hasAny) {
|
||||
@@ -987,7 +990,7 @@ const command = new Command()
|
||||
.action(bootstrap as any)
|
||||
.command(
|
||||
"generate-metadata",
|
||||
"re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`)"
|
||||
"re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate - locks`)"
|
||||
)
|
||||
.arguments("[script:file]")
|
||||
.option("--yes", "Skip confirmation prompt")
|
||||
|
||||
+286
-227
@@ -1390,244 +1390,299 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
log.info(colors.gray(`Applying changes to files ...`));
|
||||
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
|
||||
|
||||
|
||||
for await (const change of changes) {
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||
let stateExists = true;
|
||||
let stateful = opts.stateful;
|
||||
if (stateful) {
|
||||
try {
|
||||
await Deno.stat(stateTarget);
|
||||
await Deno.stat(path.join(Deno.cwd(), ".wmill"));
|
||||
} catch {
|
||||
stateExists = false;
|
||||
stateful = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Group changes by base path (before first dot)
|
||||
const groupedChanges = new Map<string, typeof changes>();
|
||||
for (const change of changes) {
|
||||
const basePath = change.path.split('.')[0];
|
||||
if (!groupedChanges.has(basePath)) {
|
||||
groupedChanges.set(basePath, []);
|
||||
}
|
||||
groupedChanges.get(basePath)!.push(change);
|
||||
}
|
||||
|
||||
|
||||
|
||||
let parallelizationFactor = opts.parallel ?? 1;
|
||||
if (parallelizationFactor <= 0) {
|
||||
parallelizationFactor = 1;
|
||||
}
|
||||
const groupedChangesArray = Array.from(groupedChanges.entries());
|
||||
log.info(`found changes for ${groupedChangesArray.length} items with a total of ${groupedChangesArray.reduce((acc, [_, changes]) => acc + changes.length, 0)} files to process`);
|
||||
if (parallelizationFactor > 1) {
|
||||
log.info(`Parallelizing ${parallelizationFactor} changes at a time`);
|
||||
}
|
||||
|
||||
// Create a pool of workers that processes items as they become available
|
||||
const pool = new Set();
|
||||
const queue = [...groupedChangesArray];
|
||||
|
||||
while (queue.length > 0 || pool.size > 0) {
|
||||
// Fill the pool until we reach parallelizationFactor
|
||||
while (pool.size < parallelizationFactor && queue.length > 0) {
|
||||
const [_basePath, changes] = queue.shift()!;
|
||||
const promise = (async () => {
|
||||
const alreadySynced: string[] = [];
|
||||
|
||||
for await (const change of changes) {
|
||||
let stateTarget = undefined;
|
||||
if (stateful) {
|
||||
try {
|
||||
stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||
await Deno.stat(stateTarget);
|
||||
} catch {
|
||||
stateTarget = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (change.name === "edited") {
|
||||
if (
|
||||
await handleScriptMetadata(
|
||||
change.path,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
globalDeps,
|
||||
codebases,
|
||||
opts
|
||||
)
|
||||
) {
|
||||
if (stateTarget) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
} else if (
|
||||
await handleFile(
|
||||
change.path,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
opts,
|
||||
globalDeps,
|
||||
codebases
|
||||
)
|
||||
) {
|
||||
if (stateTarget) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (stateTarget) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
log.info(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
}
|
||||
|
||||
if (isFileResource(change.path)) {
|
||||
const resourceFilePath = await findResourceFile(change.path);
|
||||
if (!alreadySynced.includes(resourceFilePath)) {
|
||||
alreadySynced.push(resourceFilePath);
|
||||
|
||||
const newObj = parseFromPath(
|
||||
resourceFilePath,
|
||||
await Deno.readTextFile(resourceFilePath)
|
||||
);
|
||||
|
||||
await pushResource(
|
||||
workspace.workspaceId,
|
||||
resourceFilePath,
|
||||
undefined,
|
||||
newObj
|
||||
);
|
||||
if (stateTarget) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const oldObj = parseFromPath(change.path, change.before);
|
||||
const newObj = parseFromPath(change.path, change.after);
|
||||
|
||||
await pushObj(
|
||||
workspace.workspaceId,
|
||||
change.path,
|
||||
oldObj,
|
||||
newObj,
|
||||
opts.plainSecrets ?? false,
|
||||
alreadySynced,
|
||||
opts.message
|
||||
);
|
||||
|
||||
if (stateTarget) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
} else if (change.name === "added") {
|
||||
if (
|
||||
change.path.endsWith(".script.json") ||
|
||||
change.path.endsWith(".script.yaml") ||
|
||||
change.path.endsWith(".lock") ||
|
||||
isFileResource(change.path)
|
||||
) {
|
||||
continue;
|
||||
} else if (
|
||||
await handleFile(
|
||||
change.path,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
opts,
|
||||
globalDeps,
|
||||
codebases
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (stateTarget) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
}
|
||||
const obj = parseFromPath(change.path, change.content);
|
||||
await pushObj(
|
||||
workspace.workspaceId,
|
||||
change.path,
|
||||
undefined,
|
||||
obj,
|
||||
opts.plainSecrets ?? false,
|
||||
[],
|
||||
opts.message
|
||||
);
|
||||
|
||||
if (stateTarget) {
|
||||
await Deno.writeTextFile(stateTarget, change.content);
|
||||
}
|
||||
} else if (change.name === "deleted") {
|
||||
if (change.path.endsWith(".lock")) {
|
||||
continue;
|
||||
}
|
||||
const typ = getTypeStrFromPath(change.path);
|
||||
|
||||
if (typ == "script") {
|
||||
log.info(`Archiving ${typ} ${change.path}`);
|
||||
} else {
|
||||
log.info(`Deleting ${typ} ${change.path}`);
|
||||
}
|
||||
const workspaceId = workspace.workspaceId;
|
||||
const target = change.path.replaceAll(SEP, "/");
|
||||
switch (typ) {
|
||||
case "script": {
|
||||
const script = await wmill.getScriptByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeExtensionToPath(target),
|
||||
});
|
||||
await wmill.archiveScriptByHash({
|
||||
workspace: workspaceId,
|
||||
hash: script.hash,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "folder":
|
||||
await wmill.deleteFolder({
|
||||
workspace: workspaceId,
|
||||
name: change.path.split(SEP)[1],
|
||||
});
|
||||
break;
|
||||
case "resource":
|
||||
await wmill.deleteResource({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".resource.json"),
|
||||
});
|
||||
break;
|
||||
case "resource-type":
|
||||
await wmill.deleteResourceType({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".resource-type.json"),
|
||||
});
|
||||
break;
|
||||
case "flow":
|
||||
await wmill.deleteFlowByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".flow/flow.json"),
|
||||
});
|
||||
break;
|
||||
case "app":
|
||||
await wmill.deleteApp({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".app/app.json"),
|
||||
});
|
||||
break;
|
||||
case "schedule":
|
||||
await wmill.deleteSchedule({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".schedule.json"),
|
||||
});
|
||||
break;
|
||||
case "variable":
|
||||
await wmill.deleteVariable({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".variable.json"),
|
||||
});
|
||||
break;
|
||||
case "user": {
|
||||
const users = await wmill.listUsers({
|
||||
workspace: workspaceId,
|
||||
});
|
||||
|
||||
const email = removeSuffix(
|
||||
removePathPrefix(change.path, "users"),
|
||||
".user.json"
|
||||
);
|
||||
const user = users.find((u) => u.email === email);
|
||||
if (!user) {
|
||||
throw new Error(`User ${email} not found`);
|
||||
}
|
||||
await wmill.deleteUser({
|
||||
workspace: workspaceId,
|
||||
username: user.username,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "group":
|
||||
await wmill.deleteGroup({
|
||||
workspace: workspaceId,
|
||||
name: removeSuffix(
|
||||
removePathPrefix(change.path, "groups"),
|
||||
".group.json"
|
||||
),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (stateTarget) {
|
||||
try {
|
||||
await Deno.remove(stateTarget);
|
||||
} catch {
|
||||
// state target may not exist already
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
pool.add(promise);
|
||||
// Remove from pool when complete
|
||||
promise.then(() => pool.delete(promise));
|
||||
}
|
||||
|
||||
if (change.name === "edited") {
|
||||
if (
|
||||
await handleScriptMetadata(
|
||||
change.path,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
globalDeps,
|
||||
codebases,
|
||||
opts
|
||||
)
|
||||
) {
|
||||
if (opts.stateful && stateExists) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
} else if (
|
||||
await handleFile(
|
||||
change.path,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
opts,
|
||||
globalDeps,
|
||||
codebases
|
||||
)
|
||||
) {
|
||||
if (opts.stateful && stateExists) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (opts.stateful) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
log.info(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
}
|
||||
|
||||
if (isFileResource(change.path)) {
|
||||
const resourceFilePath = await findResourceFile(change.path);
|
||||
if (!alreadySynced.includes(resourceFilePath)) {
|
||||
alreadySynced.push(resourceFilePath);
|
||||
|
||||
const newObj = parseFromPath(
|
||||
resourceFilePath,
|
||||
await Deno.readTextFile(resourceFilePath)
|
||||
);
|
||||
|
||||
await pushResource(
|
||||
workspace.workspaceId,
|
||||
resourceFilePath,
|
||||
undefined,
|
||||
newObj
|
||||
);
|
||||
if (opts.stateful && stateExists) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const oldObj = parseFromPath(change.path, change.before);
|
||||
const newObj = parseFromPath(change.path, change.after);
|
||||
|
||||
await pushObj(
|
||||
workspace.workspaceId,
|
||||
change.path,
|
||||
oldObj,
|
||||
newObj,
|
||||
opts.plainSecrets ?? false,
|
||||
alreadySynced,
|
||||
opts.message
|
||||
);
|
||||
|
||||
if (opts.stateful && stateExists) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
} else if (change.name === "added") {
|
||||
if (
|
||||
change.path.endsWith(".script.json") ||
|
||||
change.path.endsWith(".script.yaml") ||
|
||||
change.path.endsWith(".lock") ||
|
||||
isFileResource(change.path)
|
||||
) {
|
||||
continue;
|
||||
} else if (
|
||||
await handleFile(
|
||||
change.path,
|
||||
workspace,
|
||||
alreadySynced,
|
||||
opts.message,
|
||||
opts,
|
||||
globalDeps,
|
||||
codebases
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (opts.stateful && stateExists) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
|
||||
}
|
||||
const obj = parseFromPath(change.path, change.content);
|
||||
await pushObj(
|
||||
workspace.workspaceId,
|
||||
change.path,
|
||||
undefined,
|
||||
obj,
|
||||
opts.plainSecrets ?? false,
|
||||
[],
|
||||
opts.message
|
||||
);
|
||||
|
||||
if (opts.stateful && stateExists) {
|
||||
await Deno.writeTextFile(stateTarget, change.content);
|
||||
}
|
||||
} else if (change.name === "deleted") {
|
||||
if (change.path.endsWith(".lock")) {
|
||||
continue;
|
||||
}
|
||||
const typ = getTypeStrFromPath(change.path);
|
||||
|
||||
if (typ == "script") {
|
||||
log.info(`Archiving ${typ} ${change.path}`);
|
||||
} else {
|
||||
log.info(`Deleting ${typ} ${change.path}`);
|
||||
}
|
||||
const workspaceId = workspace.workspaceId;
|
||||
const target = change.path.replaceAll(SEP, "/");
|
||||
switch (typ) {
|
||||
case "script": {
|
||||
const script = await wmill.getScriptByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeExtensionToPath(target),
|
||||
});
|
||||
await wmill.archiveScriptByHash({
|
||||
workspace: workspaceId,
|
||||
hash: script.hash,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "folder":
|
||||
await wmill.deleteFolder({
|
||||
workspace: workspaceId,
|
||||
name: change.path.split(SEP)[1],
|
||||
});
|
||||
break;
|
||||
case "resource":
|
||||
await wmill.deleteResource({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".resource.json"),
|
||||
});
|
||||
break;
|
||||
case "resource-type":
|
||||
await wmill.deleteResourceType({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".resource-type.json"),
|
||||
});
|
||||
break;
|
||||
case "flow":
|
||||
await wmill.deleteFlowByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".flow/flow.json"),
|
||||
});
|
||||
break;
|
||||
case "app":
|
||||
await wmill.deleteApp({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".app/app.json"),
|
||||
});
|
||||
break;
|
||||
case "schedule":
|
||||
await wmill.deleteSchedule({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".schedule.json"),
|
||||
});
|
||||
break;
|
||||
case "variable":
|
||||
await wmill.deleteVariable({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".variable.json"),
|
||||
});
|
||||
break;
|
||||
case "user": {
|
||||
const users = await wmill.listUsers({
|
||||
workspace: workspaceId,
|
||||
});
|
||||
|
||||
const email = removeSuffix(
|
||||
removePathPrefix(change.path, "users"),
|
||||
".user.json"
|
||||
);
|
||||
const user = users.find((u) => u.email === email);
|
||||
if (!user) {
|
||||
throw new Error(`User ${email} not found`);
|
||||
}
|
||||
await wmill.deleteUser({
|
||||
workspace: workspaceId,
|
||||
username: user.username,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "group":
|
||||
await wmill.deleteGroup({
|
||||
workspace: workspaceId,
|
||||
name: removeSuffix(
|
||||
removePathPrefix(change.path, "groups"),
|
||||
".group.json"
|
||||
),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await Deno.remove(stateTarget);
|
||||
} catch {
|
||||
// state target may not exist already
|
||||
}
|
||||
// Wait for at least one task to complete before continuing
|
||||
if (pool.size > 0) {
|
||||
await Promise.race(pool);
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}.`
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1699,6 +1754,10 @@ const command = new Command()
|
||||
"--message <message:string>",
|
||||
"Include a message that will be added to all scripts/flows/apps updated during this push"
|
||||
)
|
||||
.option(
|
||||
"--parallel <number>",
|
||||
"Number of changes to process in parallel"
|
||||
)
|
||||
// deno-lint-ignore no-explicit-any
|
||||
.action(push as any);
|
||||
|
||||
|
||||
+12
-4
@@ -15,7 +15,7 @@ export function deepEqual<T>(a: T, b: T): boolean {
|
||||
if (Array.isArray(a)) {
|
||||
length = a.length;
|
||||
if (length != b.length) return false;
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (!deepEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -43,7 +43,7 @@ export function deepEqual<T>(a: T, b: T): boolean {
|
||||
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
||||
length = a.length;
|
||||
if (length != b.length) return false;
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -66,11 +66,11 @@ export function deepEqual<T>(a: T, b: T): boolean {
|
||||
length = keys.length;
|
||||
if (length !== Object.keys(b).length) return false;
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
|
||||
}
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
const key = keys[i];
|
||||
if (!deepEqual(a[key], b[key])) return false;
|
||||
}
|
||||
@@ -142,3 +142,11 @@ export function isFileResource(path: string): boolean {
|
||||
splitPath[2] == "file"
|
||||
);
|
||||
}
|
||||
|
||||
export function printSync(input: string | Uint8Array, to = Deno.stdout) {
|
||||
let bytesWritten = 0
|
||||
const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input
|
||||
while (bytesWritten < bytes.length) {
|
||||
bytesWritten += to.writeSync(bytes.subarray(bytesWritten))
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -1,3 +1,22 @@
|
||||
FROM alpine:3.14 AS oracledb-client
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Oracle DB Client for amd64
|
||||
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
|
||||
|
||||
# Oracle DB Client for arm64
|
||||
RUN mkdir -p /opt/oracle/23/arm64 \
|
||||
&& cd /opt/oracle/23/arm64 \
|
||||
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
|
||||
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
|
||||
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
|
||||
else \
|
||||
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
|
||||
fi
|
||||
|
||||
FROM ghcr.io/windmill-labs/windmill-ee:dev
|
||||
|
||||
COPY --from=rust:1.81.0 /usr/local/cargo /usr/local/cargo
|
||||
@@ -5,11 +24,12 @@ COPY --from=rust:1.81.0 /usr/local/rustup /usr/local/rustup
|
||||
|
||||
RUN pip3 install ansible
|
||||
|
||||
# dotnet SDK
|
||||
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
|
||||
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet
|
||||
ENV DOTNET_ROOT="/opt/dotnet-sdk/bin"
|
||||
|
||||
# Oracle DB Client
|
||||
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/lib
|
||||
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
|
||||
RUN apt-get -y update && apt-get install -y libaio1
|
||||
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
|
||||
|
||||
+21
-1
@@ -21,6 +21,25 @@ RUN apt-get -y update \
|
||||
RUN git clone -b master --single-branch https://github.com/google/nsjail.git . && git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
|
||||
RUN make
|
||||
|
||||
FROM alpine:3.14 AS oracledb-client
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Oracle DB Client for amd64
|
||||
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
|
||||
|
||||
# Oracle DB Client for arm64
|
||||
RUN mkdir -p /opt/oracle/23/arm64 \
|
||||
&& cd /opt/oracle/23/arm64 \
|
||||
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
|
||||
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
|
||||
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
|
||||
else \
|
||||
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
|
||||
fi
|
||||
|
||||
FROM ghcr.io/windmill-labs/windmill-ee:dev
|
||||
|
||||
RUN apt-get update && apt-get install -y libprotobuf-dev libnl-route-3-dev
|
||||
@@ -30,12 +49,13 @@ COPY --from=rust:1.80.1 /usr/local/rustup /usr/local/rustup
|
||||
|
||||
RUN pip3 install ansible
|
||||
|
||||
# dotnet SDK
|
||||
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
|
||||
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet
|
||||
ENV DOTNET_ROOT="/opt/dotnet-sdk/bin"
|
||||
|
||||
# Oracle DB Client
|
||||
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/lib
|
||||
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
|
||||
RUN apt-get -y update && apt-get install -y libaio1
|
||||
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfree = true;
|
||||
overlays = [ (import rust-overlay) ];
|
||||
};
|
||||
rust = pkgs.rust-bin.stable.latest.default.override {
|
||||
@@ -49,7 +50,7 @@
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = buildInputs ++ (with pkgs; [
|
||||
git xcaddy sqlx-cli flock sccache
|
||||
deno python3 python3Packages.pip go bun uv
|
||||
deno python3 python3Packages.pip go bun uv dotnet-sdk_9 oracle-instantclient
|
||||
]);
|
||||
packages = [
|
||||
(pkgs.writeScriptBin "wm-caddy" ''
|
||||
@@ -102,12 +103,15 @@
|
||||
UV_PATH = "${pkgs.uv}/bin/uv";
|
||||
FLOCK_PATH = "${pkgs.flock}/bin/flock";
|
||||
CARGO_PATH = "${rust}/bin/cargo";
|
||||
DOTNET_PATH = "${pkgs.dotnet-sdk_9}/bin/dotnet";
|
||||
DOTNET_ROOT = "${pkgs.dotnet-sdk_9}/share/dotnet";
|
||||
ORACLE_LIB_DIR = "${pkgs.oracle-instantclient.lib}/lib";
|
||||
};
|
||||
packages.default = self.packages.${system}.windmill;
|
||||
packages.windmill-client = pkgs.buildNpmPackage {
|
||||
name = "windmill-client";
|
||||
version = (pkgs.lib.strings.trim (builtins.readFile ./version.txt));
|
||||
|
||||
|
||||
src = pkgs.nix-gitignore.gitignoreSource [] ./frontend;
|
||||
nativeBuildInputs = with pkgs; [ pkg-config ];
|
||||
buildInputs = with pkgs; [ nodejs pixman cairo pango ];
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.452.1",
|
||||
"version": "1.455.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.452.1",
|
||||
"version": "1.455.1",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.452.1",
|
||||
"version": "1.455.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -381,11 +381,11 @@
|
||||
? 'border-2 border-green-500 bg-surface'
|
||||
: ''
|
||||
)}
|
||||
data-schema-picker
|
||||
>
|
||||
{#if diffStatus && typeof diffStatus === 'object' && diffStatus.diff !== 'same'}
|
||||
<div
|
||||
class="absolute top-0 right-2 rounded-md rounded-t-none flex flex-row overflow-hidden bg-surface"
|
||||
data-schema-picker
|
||||
>
|
||||
<button
|
||||
class="p-1 bg-green-500 text-white hover:bg-green-600"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
|
||||
export let flowPath: string | null = null
|
||||
export let headless: boolean = true
|
||||
export let addButton: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
<CaptureTable
|
||||
path={flowPath ?? ''}
|
||||
on:select
|
||||
isFlow={true}
|
||||
{headless}
|
||||
{addButton}
|
||||
on:openTriggers
|
||||
/>
|
||||
</div>
|
||||
@@ -51,6 +51,7 @@
|
||||
export let diff: Record<string, SchemaDiff> = {}
|
||||
export let disableDnd: boolean = false
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
export let isValid: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -303,6 +304,7 @@
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
{shouldDispatchChanges}
|
||||
bind:isValid
|
||||
/>
|
||||
|
||||
<slot name="runButton" />
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { type Job, JobService, type Flow, type RestartedFrom, type OpenFlow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button, Drawer, Popup } from './common'
|
||||
import { Badge, Button, Popup } from './common'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { runFlowPreview } from './flows/utils'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import SchemaFormWithArgPicker from './SchemaFormWithArgPicker.svelte'
|
||||
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
|
||||
import FlowProgressBar from './flows/FlowProgressBar.svelte'
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CornerDownLeft,
|
||||
Play,
|
||||
RefreshCw,
|
||||
X,
|
||||
ArrowLeftIcon
|
||||
} from 'lucide-svelte'
|
||||
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import SavedInputs from './SavedInputs.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { sliceModules } from './flows/flowStateUtils'
|
||||
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
|
||||
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
|
||||
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import JsonInputs from './JsonInputs.svelte'
|
||||
|
||||
export let previewMode: 'upTo' | 'whole'
|
||||
export let open: boolean
|
||||
export let preventEscape = false
|
||||
|
||||
export let jobId: string | undefined = undefined
|
||||
export let job: Job | undefined = undefined
|
||||
@@ -39,6 +32,10 @@
|
||||
|
||||
let isRunning: boolean = false
|
||||
let jobProgressReset: () => void
|
||||
let jsonView: boolean = false
|
||||
let jsonEditor: JsonInputs
|
||||
let schemaHeight = 0
|
||||
let isValid: boolean = true
|
||||
|
||||
export function test() {
|
||||
renderCount++
|
||||
@@ -83,6 +80,10 @@
|
||||
const newFlow = extractFlow(previewMode)
|
||||
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
|
||||
isRunning = true
|
||||
if (inputSelected) {
|
||||
savedArgs = $previewArgs
|
||||
inputSelected = undefined
|
||||
}
|
||||
} catch (e) {
|
||||
sendUserToast('Could not run preview', true, undefined, e.toString())
|
||||
isRunning = false
|
||||
@@ -99,6 +100,14 @@
|
||||
runPreview($previewArgs, undefined)
|
||||
}
|
||||
break
|
||||
|
||||
case 'Escape':
|
||||
if (preventEscape) {
|
||||
selectInput(undefined)
|
||||
event.preventDefault()
|
||||
event.stopPropagation
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,97 +133,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
let savedArgs = $previewArgs
|
||||
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
|
||||
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
|
||||
if (!input) {
|
||||
$previewArgs = savedArgs
|
||||
inputSelected = undefined
|
||||
setTimeout(() => {
|
||||
preventEscape = false
|
||||
}, 100)
|
||||
} else {
|
||||
$previewArgs = input
|
||||
inputSelected = type
|
||||
preventEscape = true
|
||||
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
|
||||
}
|
||||
}
|
||||
|
||||
export function refresh() {
|
||||
renderCount++
|
||||
}
|
||||
|
||||
$: if (job?.type === 'CompletedJob') {
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
let inputLibraryDrawer: Drawer
|
||||
let captureLibraryDrawer: Drawer
|
||||
let renderCount: number = 0
|
||||
|
||||
let selectedCapture: Record<string, any> | undefined = undefined
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<Drawer bind:this={inputLibraryDrawer}>
|
||||
<DrawerContent title="Input library {initialPath}" on:close={inputLibraryDrawer?.toggleDrawer}>
|
||||
<SavedInputs
|
||||
flowPath={initialPath}
|
||||
isValid={true}
|
||||
args={$previewArgs}
|
||||
on:selected_args={(e) => {
|
||||
$previewArgs = JSON.parse(JSON.stringify(e.detail))
|
||||
inputLibraryDrawer?.closeDrawer()
|
||||
renderCount++
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={captureLibraryDrawer}>
|
||||
<DrawerContent
|
||||
title="Trigger captures library {initialPath}"
|
||||
on:close={captureLibraryDrawer?.toggleDrawer}
|
||||
>
|
||||
<div class="h-full flex flex-col gap-2">
|
||||
<div class="min-h-0 grow h-full">
|
||||
<CapturesInputs
|
||||
flowPath={$pathStore}
|
||||
headless={false}
|
||||
addButton={true}
|
||||
on:select={(e) => {
|
||||
selectedCapture = e.detail
|
||||
}}
|
||||
on:openTriggers={(e) => {
|
||||
dispatch('openTriggers', e.detail)
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-2 px-2 py-2 h-[50%]">
|
||||
<div class="w-full flex flex-col">
|
||||
<Button
|
||||
color="blue"
|
||||
btnClasses="w-full"
|
||||
size="sm"
|
||||
spacingSize="xl"
|
||||
on:click={async () => {
|
||||
$previewArgs = JSON.parse(JSON.stringify(selectedCapture))
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
renderCount++
|
||||
}}
|
||||
disabled={!selectedCapture}
|
||||
>
|
||||
<ArrowLeftIcon class="w-4 h-4 mr-2" />
|
||||
Use input
|
||||
</Button>
|
||||
</div>
|
||||
<div class="w-full min-h-0 grow overflow-auto">
|
||||
{#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'}
|
||||
<div class="text-secondary mt-2">
|
||||
Payload too big to preview but can still be loaded</div
|
||||
>
|
||||
{:else if Object.keys(selectedCapture || {}).length > 0}
|
||||
<div class=" overflow-auto h-full p-2">
|
||||
<ObjectViewer json={selectedCapture} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary">
|
||||
Select an Input to preview scripts arguments
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full" id="flow-preview-content">
|
||||
<div class="flex flex-row justify-between w-full items-center gap-x-2">
|
||||
<div class="flex flex-col space-y-2 h-screen bg-surface px-4 py-2 w-full" id="flow-preview-content">
|
||||
<div class="flex flex-row w-full items-center gap-x-2">
|
||||
<div class="w-8">
|
||||
<Button
|
||||
on:click={() => dispatch('close')}
|
||||
@@ -227,28 +179,30 @@
|
||||
</div>
|
||||
|
||||
{#if isRunning}
|
||||
<Button
|
||||
color="red"
|
||||
on:click={async () => {
|
||||
isRunning = false
|
||||
try {
|
||||
jobId &&
|
||||
(await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: jobId,
|
||||
requestBody: {}
|
||||
}))
|
||||
} catch {}
|
||||
}}
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
loading={true}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div class="mx-auto">
|
||||
<Button
|
||||
color="red"
|
||||
on:click={async () => {
|
||||
isRunning = false
|
||||
try {
|
||||
jobId &&
|
||||
(await JobService.cancelQueuedJob({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: jobId,
|
||||
requestBody: {}
|
||||
}))
|
||||
} catch {}
|
||||
}}
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
loading={true}
|
||||
clickableWhileLoading
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="grow justify-center flex flex-row gap-4">
|
||||
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel}
|
||||
{#if selectedJobStepType == 'single'}
|
||||
<Button
|
||||
@@ -352,26 +306,6 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
btnClasses="h-full truncate"
|
||||
size="sm"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
captureLibraryDrawer?.openDrawer()
|
||||
}}>Trigger captures library</Button
|
||||
>
|
||||
{#if initialPath != ''}
|
||||
<Button
|
||||
btnClasses="h-full truncate"
|
||||
size="sm"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
inputLibraryDrawer?.openDrawer()
|
||||
}}>Past runs/Input library</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-col gap-y-1">
|
||||
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow}
|
||||
@@ -387,17 +321,68 @@
|
||||
<FlowProgressBar {job} bind:reset={jobProgressReset} />
|
||||
</div>
|
||||
|
||||
<div class="overflow-y-auto grow flex flex-col pr-4">
|
||||
<div class="overflow-y-auto grow flex flex-col pt-4">
|
||||
<div class="border-b">
|
||||
{#key renderCount}
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
class="py-4 max-w-3xl"
|
||||
schema={$flowStore.schema}
|
||||
bind:args={$previewArgs}
|
||||
/>
|
||||
{/key}
|
||||
<SchemaFormWithArgPicker
|
||||
runnableId={initialPath == '' ? $pathStore : initialPath}
|
||||
runnableType={'FlowPath'}
|
||||
previewArgs={$previewArgs}
|
||||
on:openTriggers
|
||||
on:select={(e) => {
|
||||
selectInput(e.detail.payload, e.detail?.type)
|
||||
}}
|
||||
{isValid}
|
||||
{jsonView}
|
||||
>
|
||||
<div class="w-full flex flex-row justify-between">
|
||||
<InputSelectedBadge {inputSelected} />
|
||||
<div class="flex flex-row gap-2">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON',
|
||||
rightTooltip: 'Fill args from JSON'
|
||||
}}
|
||||
lightMode
|
||||
on:change={(e) => {
|
||||
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
|
||||
refresh()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if jsonView}
|
||||
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
|
||||
<JsonInputs
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
$previewArgs = e.detail
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{#key renderCount}
|
||||
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
schema={$flowStore.schema}
|
||||
bind:args={$previewArgs}
|
||||
on:change={() => {
|
||||
savedArgs = $previewArgs
|
||||
}}
|
||||
bind:isValid
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</SchemaFormWithArgPicker>
|
||||
</div>
|
||||
<div class="pt-4 flex flex-col grow">
|
||||
{#if jobId}
|
||||
|
||||
@@ -821,7 +821,7 @@
|
||||
As a non logged in user, you can only see jobs ran by anonymous users like you
|
||||
</Alert>
|
||||
{:else if job}
|
||||
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl'} mx-auto px-4">
|
||||
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl px-4'} mx-auto">
|
||||
<!-- {#if innerModules.length > 0 && true}
|
||||
<h3 class="text-md leading-6 font-bold text-primay border-b pb-2">Flow result</h3>
|
||||
{:else}
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
|
||||
{#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
|
||||
<div class="w-full border rounded-lg p-2">
|
||||
<SchemaForm bind:args={default_payload} {defaultValues} {schema} />
|
||||
<SchemaForm onlyMaskPassword bind:args={default_payload} {defaultValues} {schema} />
|
||||
</div>
|
||||
{/if}
|
||||
<Tooltip
|
||||
|
||||
@@ -10,32 +10,20 @@
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import InfiniteList from './InfiniteList.svelte'
|
||||
|
||||
export let scriptHash: string | null = null
|
||||
export let scriptPath: string | null = null
|
||||
export let flowPath: string | null = null
|
||||
export let runnableId: string | undefined = undefined
|
||||
export let runnableType: RunnableType | undefined = undefined
|
||||
export let loading: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let jobs: Job[] = []
|
||||
let loading: boolean = false
|
||||
let hasMoreCurrentRuns = false
|
||||
let page = 1
|
||||
let infiniteList: InfiniteList | undefined = undefined
|
||||
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
|
||||
|
||||
let runnableType: RunnableType | undefined = undefined
|
||||
$: runnableType = scriptHash
|
||||
? 'ScriptHash'
|
||||
: scriptPath
|
||||
? 'ScriptPath'
|
||||
: flowPath
|
||||
? 'FlowPath'
|
||||
: undefined
|
||||
|
||||
let runnableId: string | undefined = undefined
|
||||
let cachedArgs: Record<string, any> = {}
|
||||
function initLoadInputs() {
|
||||
runnableId = scriptHash || scriptPath || flowPath || undefined
|
||||
|
||||
loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
const inputs = await InputService.getInputHistory({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -48,7 +36,21 @@
|
||||
|
||||
const inputsWithPayload = await Promise.all(
|
||||
inputs.map(async (input) => {
|
||||
if (cachedArgs[input.id]) {
|
||||
return {
|
||||
...input,
|
||||
payloadData: cachedArgs[input.id]
|
||||
}
|
||||
}
|
||||
const payloadData = await loadArgsFromHistory(input.id, undefined, false)
|
||||
if (payloadData === 'WINDMILL_TOO_BIG') {
|
||||
return {
|
||||
...input,
|
||||
payloadData: 'WINDMILL_TOO_BIG',
|
||||
getFullPayload: () => loadArgsFromHistory(input.id, undefined, true)
|
||||
}
|
||||
}
|
||||
cachedArgs[input.id] = payloadData
|
||||
return {
|
||||
...input,
|
||||
payloadData
|
||||
@@ -60,21 +62,24 @@
|
||||
infiniteList?.setLoader(loadInputsPageFn)
|
||||
}
|
||||
|
||||
function handleSelected(data: any) {
|
||||
async function handleSelected(data: any) {
|
||||
if (selected === data.id) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
return
|
||||
}
|
||||
selected = data.id
|
||||
dispatch('select', data.payloadData)
|
||||
if (data.payloadData === 'WINDMILL_TOO_BIG') {
|
||||
const fullPayload = await data.getFullPayload?.()
|
||||
dispatch('select', fullPayload)
|
||||
} else {
|
||||
dispatch('select', structuredClone(data.payloadData))
|
||||
}
|
||||
}
|
||||
|
||||
let selected: string | undefined = undefined
|
||||
|
||||
onDestroy(() => {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
})
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
@@ -100,8 +105,7 @@
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && selected) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
@@ -115,14 +119,21 @@
|
||||
|
||||
let jobHovered: string | undefined = undefined
|
||||
|
||||
function refresh() {
|
||||
export function refresh() {
|
||||
if (infiniteList) {
|
||||
infiniteList.loadData('refresh')
|
||||
}
|
||||
}
|
||||
|
||||
export function resetSelected(dispatchEvent?: boolean) {
|
||||
selected = undefined
|
||||
if (dispatchEvent) {
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
$: !loading && refresh()
|
||||
$: $workspaceStore && (scriptHash || scriptPath || flowPath) && infiniteList && initLoadInputs()
|
||||
$: $workspaceStore && runnableId && runnableType && infiniteList && initLoadInputs()
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
@@ -153,19 +164,18 @@
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
if (selected) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="grow-0">
|
||||
<div class="grow-0" data-schema-picker>
|
||||
<DataTable size="xs" bind:currentPage={page} hasMore={hasMoreCurrentRuns} tableFixed={true}>
|
||||
{#if loading && (jobs == undefined || jobs?.length == 0)}
|
||||
<div class="text-center text-tertiary text-xs py-2">Loading current runs...</div>
|
||||
{:else if jobs?.length > 0}
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col class="w-20" />
|
||||
<col class="w-16" />
|
||||
<col />
|
||||
</colgroup>
|
||||
|
||||
@@ -190,7 +200,7 @@
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 grow">
|
||||
<div class="min-h-0 grow" data-schema-picker>
|
||||
<InfiniteList
|
||||
bind:this={infiniteList}
|
||||
selectedItemId={selected}
|
||||
@@ -200,7 +210,7 @@
|
||||
<svelte:fragment slot="columns">
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col class="w-20" />
|
||||
<col class="w-16" />
|
||||
<col />
|
||||
</colgroup>
|
||||
</svelte:fragment>
|
||||
@@ -213,7 +223,9 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="empty">
|
||||
<div class="text-center text-tertiary text-xs py-2">No previous Runs</div>
|
||||
<div class="text-center text-tertiary text-xs py-2">
|
||||
{runnableId ? 'No previous inputs' : 'Save draft to see previous runs'}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</InfiniteList>
|
||||
</div>
|
||||
|
||||
@@ -56,10 +56,14 @@
|
||||
isNew: false
|
||||
}))
|
||||
}
|
||||
}, 1000)
|
||||
}, 2000)
|
||||
|
||||
page = Math.floor(items.length / perPage) + 1
|
||||
hasMore = items.length === perPage * (page - 1)
|
||||
page = Math.ceil(items.length / perPage)
|
||||
hasMore = items.length === perPage * page
|
||||
if (hasMore) {
|
||||
const potentialNewItems = await loadInputs(page + 1, perPage)
|
||||
hasMore = potentialNewItems.length > 0
|
||||
}
|
||||
initLoad = true
|
||||
isEmpty = items.length === 0
|
||||
length = items.length
|
||||
@@ -112,6 +116,7 @@
|
||||
on:loadMore={() => {
|
||||
loadData()
|
||||
}}
|
||||
{loading}
|
||||
>
|
||||
<slot name="columns" />
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let updateOnBlur = true
|
||||
export let placeholder =
|
||||
'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}'
|
||||
|
||||
let pendingJson = ''
|
||||
let simpleEditor: SimpleEditor | undefined = undefined
|
||||
|
||||
function updatePayloadFromJson(jsonInput: string) {
|
||||
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
|
||||
dispatch('select', undefined)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
dispatch('select', parsed)
|
||||
} catch (error) {
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export function setCode(code: string) {
|
||||
simpleEditor?.setCode(code)
|
||||
}
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
const elements = document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
|
||||
return elements ? (Array.from(elements) as HTMLElement[]) : []
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-full"
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
if (updateOnBlur) {
|
||||
updatePayloadFromJson('')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SimpleEditor
|
||||
bind:this={simpleEditor}
|
||||
on:focus={() => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('focus')
|
||||
updatePayloadFromJson(pendingJson)
|
||||
}
|
||||
}}
|
||||
on:blur={async () => {
|
||||
if (updateOnBlur) {
|
||||
dispatch('blur')
|
||||
}
|
||||
}}
|
||||
on:change={(e) => {
|
||||
if (e.detail?.code !== undefined) {
|
||||
updatePayloadFromJson(e.detail.code)
|
||||
}
|
||||
}}
|
||||
bind:code={pendingJson}
|
||||
lang="json"
|
||||
class="h-full"
|
||||
{placeholder}
|
||||
/>
|
||||
</div>
|
||||
@@ -18,6 +18,7 @@
|
||||
import RunFormAdvancedPopup from './RunFormAdvancedPopup.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { replaceState } from '$app/navigation'
|
||||
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
||||
|
||||
export let runnable:
|
||||
| {
|
||||
@@ -54,9 +55,12 @@
|
||||
export let overrideTag: string | undefined
|
||||
|
||||
export let args: Record<string, any> = {}
|
||||
export let jsonView = false
|
||||
|
||||
let reloadArgs = 0
|
||||
let blockPopupOpen = false
|
||||
let jsonEditor: JsonInputs | undefined = undefined
|
||||
let schemaHeight = 0
|
||||
|
||||
export async function setArgs(nargs: Record<string, any>) {
|
||||
args = nargs
|
||||
@@ -89,6 +93,10 @@
|
||||
console.error('Impossible to set hash in args', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function setCode(code: string) {
|
||||
jsonEditor?.setCode(code)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="max-w-3xl">
|
||||
@@ -137,7 +145,7 @@
|
||||
{#if topButton}
|
||||
<Button
|
||||
btnClasses="!px-6 !py-1 w-full"
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || jsonView}
|
||||
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
|
||||
>
|
||||
{buttonText}
|
||||
@@ -147,22 +155,37 @@
|
||||
<div class="my-2" />
|
||||
{#if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
|
||||
<div class="text-sm py-4 italic">No arguments</div>
|
||||
{:else if jsonView}
|
||||
<div class="py-2" style="height: {schemaHeight}px" data-schema-picker>
|
||||
<JsonInputs
|
||||
bind:this={jsonEditor}
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
args = e.detail
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{#key reloadArgs}
|
||||
<SchemaForm
|
||||
helperScript={runnable.hash
|
||||
? {
|
||||
type: 'hash',
|
||||
hash: runnable.hash
|
||||
}
|
||||
: undefined}
|
||||
prettifyHeader
|
||||
{noVariablePicker}
|
||||
{autofocus}
|
||||
schema={runnable.schema}
|
||||
bind:isValid
|
||||
bind:args
|
||||
/>
|
||||
<div bind:clientHeight={schemaHeight}>
|
||||
<SchemaForm
|
||||
helperScript={runnable.hash
|
||||
? {
|
||||
type: 'hash',
|
||||
hash: runnable.hash
|
||||
}
|
||||
: undefined}
|
||||
prettifyHeader
|
||||
{noVariablePicker}
|
||||
{autofocus}
|
||||
schema={runnable.schema}
|
||||
bind:isValid
|
||||
bind:args
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -176,7 +199,7 @@
|
||||
{loading}
|
||||
color="dark"
|
||||
btnClasses="!px-6 !py-1 !h-8 inline-flex gap-2"
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || jsonView}
|
||||
on:click={() => runAction(scheduledForStr, args, invisible_to_owner, overrideTag)}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
|
||||
>
|
||||
@@ -230,7 +253,7 @@
|
||||
{:else if !topButton}
|
||||
<Button
|
||||
btnClasses="!px-6 !py-1 w-full"
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || jsonView}
|
||||
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
|
||||
>
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<CronInput bind:schedule={$primarySchedule.cron} bind:timezone={$primarySchedule.timezone} />
|
||||
<SchemaForm {schema} bind:args={$primarySchedule.args} />
|
||||
<SchemaForm onlyMaskPassword {schema} bind:args={$primarySchedule.args} />
|
||||
{#if emptyString($primarySchedule.cron)}
|
||||
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores.js'
|
||||
import { sendUserToast } from '$lib/utils.js'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Edit, Trash2, Save } from 'lucide-svelte'
|
||||
import { Edit, Trash2, Save, Eye } from 'lucide-svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { Cell } from './table/index'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
@@ -12,15 +12,23 @@
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import InfiniteList from './InfiniteList.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import PopoverV2 from '$lib/components/meltComponents/Popover.svelte'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
|
||||
export let flowPath: string | null = null
|
||||
export let previewArgs: any = undefined
|
||||
export let runnableId: string | undefined = undefined
|
||||
export let runnableType: RunnableType | undefined = undefined
|
||||
export let isValid: boolean = false
|
||||
export let noButton: boolean = false
|
||||
export let jsonView: boolean = false
|
||||
|
||||
interface EditableInput extends Input {
|
||||
isEditing?: boolean
|
||||
isSaving?: boolean
|
||||
isNew?: boolean
|
||||
isDeleting?: boolean
|
||||
payloadData?: any
|
||||
getFullPayload?: () => Promise<any>
|
||||
}
|
||||
|
||||
let infiniteList: InfiniteList | null = null
|
||||
@@ -30,10 +38,6 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: runnableId = flowPath || undefined
|
||||
let runnableType: RunnableType | undefined = undefined
|
||||
$: runnableType = flowPath ? 'FlowPath' : undefined
|
||||
|
||||
async function updateInput(input: EditableInput | null) {
|
||||
if (!input) return
|
||||
input.isSaving = true
|
||||
@@ -57,13 +61,30 @@
|
||||
|
||||
function initLoadInputs() {
|
||||
const loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
return await InputService.listInputs({
|
||||
const inputs = await InputService.listInputs({
|
||||
workspace: $workspaceStore!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
const inputsWithPayload = await Promise.all(
|
||||
inputs.map(async (input) => {
|
||||
const payloadData = await loadLargeArgs(input.id, undefined, false)
|
||||
if (payloadData === 'WINDMILL_TOO_BIG') {
|
||||
return {
|
||||
...input,
|
||||
payloadData: 'WINDMILL_TOO_BIG',
|
||||
getFullPayload: () => loadLargeArgs(input.id, undefined, true)
|
||||
}
|
||||
}
|
||||
return {
|
||||
...input,
|
||||
payloadData
|
||||
}
|
||||
})
|
||||
)
|
||||
return inputsWithPayload
|
||||
}
|
||||
infiniteList?.setLoader(loadInputsPageFn)
|
||||
|
||||
@@ -95,20 +116,21 @@
|
||||
if (input.isEditing) return
|
||||
|
||||
if (selectedInput === input.id) {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
} else {
|
||||
selectedInput = input.id
|
||||
selectedArgs = await loadLargeArgs(input.id, true, false)
|
||||
dispatch('select', selectedArgs)
|
||||
if (input.payloadData === 'WINDMILL_TOO_BIG') {
|
||||
const fullPayload = await input.getFullPayload?.()
|
||||
dispatch('select', fullPayload)
|
||||
} else {
|
||||
selectedArgs = structuredClone(input.payloadData ?? {})
|
||||
dispatch('select', selectedArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
})
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
@@ -122,9 +144,7 @@
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'Escape' && selectedInput) {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
@@ -151,42 +171,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: $workspaceStore && flowPath && (infiniteList && initLoadInputs(), (draft = false))
|
||||
export function refresh() {
|
||||
infiniteList?.loadData('refresh')
|
||||
}
|
||||
|
||||
export function resetSelected(dispatchEvent?: boolean) {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
if (dispatchEvent) {
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
$: $workspaceStore &&
|
||||
runnableId &&
|
||||
runnableType &&
|
||||
(infiniteList && initLoadInputs(), (draft = false))
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div
|
||||
class="w-full flex flex-col gap-1 h-full overflow-y-auto p"
|
||||
class="w-full flex flex-col gap-1 h-full overflow-y-auto"
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
resetSelected(true)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Popover class="w-full" placement="bottom" disablePopup={flowPath && previewArgs}>
|
||||
<svelte:fragment slot="text">
|
||||
{#if !flowPath}
|
||||
Save draft first before you can save inputs
|
||||
{:else if !previewArgs}
|
||||
Add inputs before saving
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<SaveInputsButton
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
args={previewArgs ?? {}}
|
||||
disabled={!previewArgs || !flowPath}
|
||||
on:update={() => {
|
||||
infiniteList?.loadData('refresh')
|
||||
}}
|
||||
showTooltip={true}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="grow min-h-0">
|
||||
{#if !noButton}
|
||||
<div>
|
||||
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
|
||||
<svelte:fragment slot="text">
|
||||
{#if !runnableId}
|
||||
Save draft first before you can save inputs
|
||||
{:else if !previewArgs}
|
||||
Add inputs before saving
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<SaveInputsButton
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
args={previewArgs ?? {}}
|
||||
disabled={!previewArgs || !runnableId || !isValid || jsonView}
|
||||
on:update={() => {
|
||||
refresh()
|
||||
}}
|
||||
showTooltip={true}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grow min-h-0" data-schema-picker>
|
||||
{#if !draft}
|
||||
<InfiniteList
|
||||
bind:this={infiniteList}
|
||||
@@ -208,9 +243,9 @@
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div
|
||||
class="w-full flex items-center text-sm justify-between gap-4 px-4 text-left transition-all"
|
||||
class="w-full flex items-center text-sm justify-between gap-4 py-1 text-left transition-all"
|
||||
>
|
||||
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0 p-1">
|
||||
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
|
||||
{#if isEditing && isEditing.id === item.id}
|
||||
<form
|
||||
on:submit={() => {
|
||||
@@ -230,6 +265,25 @@
|
||||
{/if}
|
||||
{#if item.created_by == $userStore?.username || $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<div class="items-center flex gap-2">
|
||||
<PopoverV2 displayArrow={false} closeButton={false} openOnHover={true}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Eye class="w-4 h-4 group-hover:block hidden" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="p-2">
|
||||
{#if item.payloadData === 'WINDMILL_TOO_BIG'}
|
||||
<div class="text-center text-tertiary text-xs">
|
||||
Payload too big to preview but can still be loaded
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-w-60 overflow-auto">
|
||||
<ObjectViewer json={item.payloadData} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</PopoverV2>
|
||||
|
||||
{#if !isEditing || isEditing?.id !== item.id}
|
||||
<div class="group-hover:block hidden -my-2">
|
||||
<Toggle
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
|
||||
import { type RunnableType } from '$lib/gen/types.gen.js'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import SaveInputsButton from '$lib/components/SaveInputsButton.svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let scriptHash: string | null = null
|
||||
export let scriptPath: string | null = null
|
||||
export let flowPath: string | null = null
|
||||
export let inputSelected: 'saved' | 'history' | undefined = undefined
|
||||
export let jsonView: boolean = false
|
||||
|
||||
// Are the current Inputs valid and able to be saved?
|
||||
export let isValid: boolean
|
||||
export let args: object
|
||||
|
||||
let savedArgs: any = undefined
|
||||
let runnableType: RunnableType | undefined = undefined
|
||||
let savedInputsPicker: SavedInputsPicker | undefined = undefined
|
||||
let historicInputs: HistoricInputs | undefined = undefined
|
||||
|
||||
$: runnableId = scriptHash || scriptPath || flowPath || undefined
|
||||
$: runnableType = scriptHash
|
||||
? 'ScriptHash'
|
||||
: scriptPath
|
||||
? 'ScriptPath'
|
||||
: flowPath
|
||||
? 'FlowPath'
|
||||
: undefined
|
||||
|
||||
function selectArgs(selected_args: any, type: 'saved' | 'history' | undefined) {
|
||||
if (selected_args) {
|
||||
if (!inputSelected) {
|
||||
savedArgs = args
|
||||
}
|
||||
inputSelected = type
|
||||
dispatch('selected_args', selected_args)
|
||||
} else if (savedArgs) {
|
||||
inputSelected = type
|
||||
dispatch('selected_args', savedArgs)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-w-[300px] h-full flex flex-col">
|
||||
<Splitpanes horizontal={true}>
|
||||
<Pane class="px-4 py-2 h-full">
|
||||
<Section
|
||||
label="Saved Inputs"
|
||||
tooltip="Shared inputs are available to anyone with access to the script"
|
||||
wrapperClass="h-full"
|
||||
small={true}
|
||||
>
|
||||
<svelte:fragment slot="action">
|
||||
<SaveInputsButton
|
||||
{args}
|
||||
disabled={!isValid || jsonView}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:update={() => {
|
||||
savedInputsPicker?.refresh()
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<SavedInputsPicker
|
||||
bind:this={savedInputsPicker}
|
||||
previewArgs={args}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
{isValid}
|
||||
on:select={(e) => {
|
||||
if (e.detail) historicInputs?.resetSelected()
|
||||
selectArgs(e.detail, e.detail ? 'saved' : undefined)
|
||||
}}
|
||||
noButton={true}
|
||||
/>
|
||||
</Section>
|
||||
</Pane>
|
||||
|
||||
<Pane class="px-4 py-4 h-full">
|
||||
<Section label="History" wrapperClass="h-full" small={true}>
|
||||
<HistoricInputs
|
||||
bind:this={historicInputs}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:select={(e) => {
|
||||
if (e.detail) savedInputsPicker?.resetSelected()
|
||||
selectArgs(e.detail, e.detail ? 'history' : undefined)
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import FlowInputEditor from '$lib/components/flows/content/FlowInputEditor.svelte'
|
||||
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
|
||||
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
|
||||
import { History, Save } from 'lucide-svelte'
|
||||
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
|
||||
import CaptureButton from './triggers/CaptureButton.svelte'
|
||||
import SavedInputsPicker from './SavedInputsPicker.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import CaptureTable from './triggers/CaptureTable.svelte'
|
||||
import RefreshButton from './common/button/RefreshButton.svelte'
|
||||
|
||||
export let runnableId: string = ''
|
||||
export let runnableType: any
|
||||
export let previewArgs: any
|
||||
export let isValid: boolean = true
|
||||
export let jsonView: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const getDropdownItems = () => {
|
||||
return [
|
||||
{
|
||||
label: 'History',
|
||||
onClick: () => {
|
||||
if (selectedTab === 'history') {
|
||||
selectedTab = undefined
|
||||
rightPanelOpen = false
|
||||
} else {
|
||||
selectedTab = 'history'
|
||||
rightPanelOpen = true
|
||||
}
|
||||
},
|
||||
icon: History,
|
||||
selected: selectedTab === 'history'
|
||||
},
|
||||
{
|
||||
label: 'Saved inputs',
|
||||
onClick: () => {
|
||||
if (selectedTab === 'saved_inputs') {
|
||||
selectedTab = undefined
|
||||
rightPanelOpen = false
|
||||
} else {
|
||||
selectedTab = 'saved_inputs'
|
||||
rightPanelOpen = true
|
||||
}
|
||||
},
|
||||
icon: Save,
|
||||
selected: selectedTab === 'saved_inputs'
|
||||
},
|
||||
{
|
||||
label: 'Trigger captures',
|
||||
onClick: () => {
|
||||
if (selectedTab === 'captures') {
|
||||
selectedTab = undefined
|
||||
rightPanelOpen = false
|
||||
} else {
|
||||
selectedTab = 'captures'
|
||||
rightPanelOpen = true
|
||||
}
|
||||
},
|
||||
icon: CaptureIcon,
|
||||
selected: selectedTab === 'captures'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let rightHeight = 0
|
||||
let selectedTab: 'history' | 'saved_inputs' | 'captures' | undefined = undefined
|
||||
let dropdownItems: any
|
||||
let rightPanelOpen = false
|
||||
|
||||
let loading = false
|
||||
let historicInputs: HistoricInputs | undefined = undefined
|
||||
$: selectedTab, (dropdownItems = getDropdownItems())
|
||||
</script>
|
||||
|
||||
<div class="h-fit">
|
||||
<Splitpanes class={!rightPanelOpen ? 'splitter-hidden' : ''}>
|
||||
<Pane class="!overflow-visible" size={70} minSize={30}>
|
||||
<div class="relative w-full h-fit pr-12 pb-4 overflow-hidden" bind:clientHeight={rightHeight}>
|
||||
<div class="absolute right-[1px] -top-[1px] z-50">
|
||||
<SideBarTab
|
||||
expandRight={rightPanelOpen}
|
||||
{dropdownItems}
|
||||
fullMenu={true}
|
||||
noTrigger={true}
|
||||
/>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
<Pane minSize={rightPanelOpen ? 30 : 0}>
|
||||
{#if rightPanelOpen}
|
||||
<div style="height: {rightHeight}px" class="border-t border-r pb-2">
|
||||
{#if selectedTab === 'history'}
|
||||
<FlowInputEditor title="History">
|
||||
<svelete:fragment slot="action">
|
||||
<div class="center-center">
|
||||
<RefreshButton {loading} on:click={() => historicInputs?.refresh()} />
|
||||
</div>
|
||||
</svelete:fragment>
|
||||
<HistoricInputs
|
||||
bind:loading
|
||||
bind:this={historicInputs}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail, type: 'history' })
|
||||
}}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if selectedTab === 'saved_inputs'}
|
||||
<FlowInputEditor title="Saved inputs">
|
||||
<SavedInputsPicker
|
||||
{isValid}
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
{previewArgs}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail, type: 'saved' })
|
||||
}}
|
||||
{jsonView}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if selectedTab === 'captures'}
|
||||
<FlowInputEditor title="Trigger captures">
|
||||
<svelete:fragment slot="action">
|
||||
<div class="center-center">
|
||||
<CaptureButton on:openTriggers small={true} />
|
||||
</div>
|
||||
</svelete:fragment>
|
||||
<div class="h-full">
|
||||
<CaptureTable
|
||||
path={runnableId}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail, type: 'captures' })
|
||||
}}
|
||||
isFlow={true}
|
||||
headless={true}
|
||||
addButton={false}
|
||||
/>
|
||||
</div>
|
||||
</FlowInputEditor>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.splitter-hidden .splitpanes__splitter) {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -9,13 +9,14 @@
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let eeOnly = false
|
||||
export let small: boolean = false
|
||||
export let wrapperClass: string = ''
|
||||
|
||||
export let collapsable: boolean = false
|
||||
export let collapsed: boolean = true
|
||||
export let headless: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-col">
|
||||
<div class={twMerge('w-full flex flex-col', wrapperClass)}>
|
||||
{#if !headless}
|
||||
<div class="flex flex-row justify-between items-center mb-2">
|
||||
<h2
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
|
||||
>
|
||||
<SchemaForm
|
||||
onlyMaskPassword
|
||||
defaultValues={resolvedConfig.defaultValues}
|
||||
dynamicEnums={resolvedConfig.dynamicEnums}
|
||||
schema={result}
|
||||
|
||||
@@ -156,5 +156,5 @@
|
||||
</script>
|
||||
|
||||
{#if schema}
|
||||
<SchemaForm {schema} bind:args />
|
||||
<SchemaForm onlyMaskPassword {schema} bind:args />
|
||||
{/if}
|
||||
|
||||
@@ -477,12 +477,27 @@ export async function getDbSchemas(
|
||||
if (!testResult.success) {
|
||||
console.error(testResult.result?.['error']?.['message'])
|
||||
} else {
|
||||
if (testResult.result === 'WINDMILL_TOO_BIG') {
|
||||
console.info('Result is too big, fetching result separately')
|
||||
const data = await JobService.getCompletedJobResult({
|
||||
workspace,
|
||||
id: job
|
||||
})
|
||||
testResult.result = data
|
||||
}
|
||||
if (resourceType !== undefined) {
|
||||
if (resourceType !== 'graphql') {
|
||||
const { processingFn } = scripts[resourceType]
|
||||
const schema: any =
|
||||
processingFn !== undefined ? processingFn(testResult.result) : testResult.result
|
||||
|
||||
let schema: any
|
||||
try {
|
||||
schema =
|
||||
processingFn !== undefined ? processingFn(testResult.result) : testResult.result
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
errorCallback('Error processing schema')
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const dbSchema = {
|
||||
lang: resourceTypeToLang(resourceType) as SQLSchema['lang'],
|
||||
schema,
|
||||
|
||||
@@ -769,6 +769,7 @@
|
||||
{#if render && (autoRefresh || forceSchemaDisplay) && schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0}
|
||||
<div class="px-2 h-fit min-h-0">
|
||||
<SchemaForm
|
||||
onlyMaskPassword
|
||||
schema={schemaStripped}
|
||||
appPath={defaultIfEmptyString($appPath, `u/${$userStore?.username ?? 'unknown'}/newapp`)}
|
||||
{computeS3ForceViewerPolicies}
|
||||
|
||||
@@ -650,6 +650,7 @@ export async function main(app_path: string, startup_duration = 5, kind: 'pdf' |
|
||||
{#if selectedTab !== 'custom' || customPath !== undefined}
|
||||
{#key selectedTab + JSON.stringify(customPathSchema ?? {})}
|
||||
<SchemaForm
|
||||
onlyMaskPassword
|
||||
bind:isValid={areArgsValid}
|
||||
bind:args
|
||||
schema={selectedTab !== 'custom'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { AlertType } from './model'
|
||||
import { type AlertType, classes } from './model'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -38,34 +38,6 @@
|
||||
success: CheckCircle2
|
||||
}
|
||||
|
||||
const classes: Record<AlertType, Record<string, string>> = {
|
||||
info: {
|
||||
bgClass: 'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40',
|
||||
iconClass: 'text-blue-500 dark:text-blue-300',
|
||||
titleClass: 'text-blue-800 dark:text-blue-100',
|
||||
descriptionClass: 'text-blue-700 dark:text-blue-100'
|
||||
},
|
||||
warning: {
|
||||
bgClass:
|
||||
'bg-yellow-50 border-yellow-200 border dark:bg-yellow-900/40 dark:border-yellow-700/40',
|
||||
iconClass: 'text-yellow-500 dark:text-yellow-300/90',
|
||||
titleClass: 'text-yellow-800 dark:text-yellow-100/90',
|
||||
descriptionClass: 'text-yellow-700 dark:text-yellow-100/90'
|
||||
},
|
||||
error: {
|
||||
bgClass: 'bg-red-50 border-red-200 border dark:bg-red-900/40 dark:border-red-700/40',
|
||||
iconClass: 'text-red-500 dark:text-red-300/90',
|
||||
titleClass: 'text-red-800 dark:text-red-100/90',
|
||||
descriptionClass: 'text-red-700 dark:text-red-100/90'
|
||||
},
|
||||
success: {
|
||||
bgClass: 'bg-green-50 border-green-200 border dark:bg-green-900/40 dark:border-green-700/40',
|
||||
iconClass: 'text-green-500 dark:text-green-300/90',
|
||||
titleClass: 'text-green-800 dark:text-green-100/90',
|
||||
descriptionClass: 'text-green-700 dark:text-green-100/90'
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollapse() {
|
||||
if (collapsible) {
|
||||
isCollapsed = !isCollapsed
|
||||
|
||||
@@ -1 +1,29 @@
|
||||
export type AlertType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export const classes: Record<AlertType, Record<string, string>> = {
|
||||
info: {
|
||||
bgClass: 'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40',
|
||||
iconClass: 'text-blue-500 dark:text-blue-300',
|
||||
titleClass: 'text-blue-800 dark:text-blue-100',
|
||||
descriptionClass: 'text-blue-700 dark:text-blue-100'
|
||||
},
|
||||
warning: {
|
||||
bgClass:
|
||||
'bg-yellow-50 border-yellow-200 border dark:bg-yellow-900/40 dark:border-yellow-700/40',
|
||||
iconClass: 'text-yellow-500 dark:text-yellow-300/90',
|
||||
titleClass: 'text-yellow-800 dark:text-yellow-100/90',
|
||||
descriptionClass: 'text-yellow-700 dark:text-yellow-100/90'
|
||||
},
|
||||
error: {
|
||||
bgClass: 'bg-red-50 border-red-200 border dark:bg-red-900/40 dark:border-red-700/40',
|
||||
iconClass: 'text-red-500 dark:text-red-300/90',
|
||||
titleClass: 'text-red-800 dark:text-red-100/90',
|
||||
descriptionClass: 'text-red-700 dark:text-red-100/90'
|
||||
},
|
||||
success: {
|
||||
bgClass: 'bg-green-50 border-green-200 border dark:bg-green-900/40 dark:border-green-700/40',
|
||||
iconClass: 'text-green-500 dark:text-green-300/90',
|
||||
titleClass: 'text-green-800 dark:text-green-100/90',
|
||||
descriptionClass: 'text-green-700 dark:text-green-100/90'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
export let open = false
|
||||
export let id = (Math.random() + 1).toString(36).substring(10)
|
||||
export let preventEscape = false
|
||||
|
||||
if (open) {
|
||||
openedDrawers.push(id)
|
||||
@@ -63,7 +64,10 @@
|
||||
if (open) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) {
|
||||
if (
|
||||
(id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) &&
|
||||
!preventEscape
|
||||
) {
|
||||
openedDrawers.pop()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
export let alwaysOpen = false
|
||||
export let shouldUsePortal: boolean = true
|
||||
export let offset: number = 0
|
||||
export let preventEscape = false
|
||||
|
||||
let disposable: Disposable | undefined = undefined
|
||||
|
||||
@@ -71,6 +72,7 @@
|
||||
bind:this={disposable}
|
||||
on:open
|
||||
on:close
|
||||
{preventEscape}
|
||||
>
|
||||
<aside
|
||||
class="drawer windmill-app windmill-drawer {$$props.class ?? ''} {$$props.positionClass ??
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<Tabs bind:selected wrapperClass="flex-none w-full">
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
<Tab value="saved_inputs">Inputs library</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<Tab value="websockets">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
Websockets
|
||||
WebSockets
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="postgres">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import JsonInputs from '$lib/components/JsonInputs.svelte'
|
||||
import { convert } from '@redocly/json-to-json-schema'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
|
||||
@@ -29,7 +29,6 @@
|
||||
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
|
||||
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
|
||||
import FlowInputEditor from './FlowInputEditor.svelte'
|
||||
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
|
||||
import {
|
||||
@@ -42,6 +41,7 @@
|
||||
applyDiff
|
||||
} from '$lib/components/schema/schemaUtils'
|
||||
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let disabled: boolean
|
||||
@@ -49,7 +49,6 @@
|
||||
const { flowStore, previewArgs, pathStore, initialPath, flowInputEditorState } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let pendingJson: string
|
||||
let addProperty: AddPropertyV2 | undefined = undefined
|
||||
let previewSchema: Record<string, any> | undefined = undefined
|
||||
let payloadData: Record<string, any> | undefined = undefined
|
||||
@@ -65,6 +64,8 @@
|
||||
let runDisabled: boolean = false
|
||||
let editableSchemaForm: EditableSchemaForm | undefined = undefined
|
||||
let savedPreviewArgs: Record<string, any> | undefined = undefined
|
||||
let preventEscape = false
|
||||
let isValid = true
|
||||
|
||||
function updateEditPanelSize(size: number | undefined) {
|
||||
if (!$flowInputEditorState) return
|
||||
@@ -244,19 +245,6 @@
|
||||
previewArguments = structuredClone(payloadData)
|
||||
}
|
||||
|
||||
function updatePayloadFromJson(jsonInput: string) {
|
||||
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
updatePreviewSchemaAndArgs(parsed)
|
||||
} catch (error) {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
let tabButtonWidth = 0
|
||||
|
||||
let connectFirstNode: () => void = () => {}
|
||||
@@ -325,7 +313,7 @@
|
||||
<!-- Add svelte:window to listen for keyboard events -->
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
|
||||
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
|
||||
<FlowPreviewContent
|
||||
bind:this={flowPreviewContent}
|
||||
open={previewOpen}
|
||||
@@ -333,6 +321,7 @@
|
||||
on:close={() => {
|
||||
previewOpen = false
|
||||
}}
|
||||
bind:preventEscape
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
@@ -378,6 +367,7 @@
|
||||
on:schemaChange={() => {
|
||||
resetArgs()
|
||||
}}
|
||||
bind:isValid
|
||||
>
|
||||
<svelte:fragment slot="openEditTab">
|
||||
<div class={twMerge('flex flex-row divide-x', ButtonType.ColorVariants.blue.divider)}>
|
||||
@@ -470,9 +460,8 @@
|
||||
}}
|
||||
>
|
||||
<HistoricInputs
|
||||
scriptHash={null}
|
||||
scriptPath={null}
|
||||
flowPath={$pathStore}
|
||||
runnableId={initialPath ?? undefined}
|
||||
runnableType={$pathStore ? 'FlowPath' : undefined}
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
@@ -490,12 +479,17 @@
|
||||
<CaptureButton on:openTriggers small={true} />
|
||||
</div>
|
||||
</svelete:fragment>
|
||||
<CapturesInputs
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
flowPath={$pathStore}
|
||||
/>
|
||||
<div class="h-full">
|
||||
<CaptureTable
|
||||
path={$pathStore}
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
isFlow={true}
|
||||
headless={true}
|
||||
addButton={false}
|
||||
/>
|
||||
</div>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'}
|
||||
<FlowInputEditor
|
||||
@@ -505,7 +499,8 @@
|
||||
title="Saved inputs"
|
||||
>
|
||||
<SavedInputsPicker
|
||||
flowPath={initialPath}
|
||||
runnableId={initialPath ?? undefined}
|
||||
runnableType={$pathStore ? 'FlowPath' : undefined}
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
@@ -513,6 +508,7 @@
|
||||
preventEnter = e.detail
|
||||
}}
|
||||
previewArgs={previewArguments}
|
||||
{isValid}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'json'}
|
||||
@@ -522,26 +518,16 @@
|
||||
}}
|
||||
title="Json payload"
|
||||
>
|
||||
<SimpleEditor
|
||||
<JsonInputs
|
||||
on:focus={() => {
|
||||
preventEnter = true
|
||||
updatePayloadFromJson(pendingJson)
|
||||
}}
|
||||
on:blur={async () => {
|
||||
preventEnter = false
|
||||
setTimeout(() => {
|
||||
if (payloadData) {
|
||||
updatePayloadFromJson('')
|
||||
}
|
||||
}, 100)
|
||||
}}
|
||||
on:change={(e) => {
|
||||
updatePayloadFromJson(e.detail.code)
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
bind:code={pendingJson}
|
||||
lang="json"
|
||||
class="h-full"
|
||||
placeholder={'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}'}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'}
|
||||
@@ -575,7 +561,7 @@
|
||||
<Button
|
||||
color="dark"
|
||||
btnClasses="w-fit"
|
||||
disabled={runDisabled}
|
||||
disabled={runDisabled || !isValid}
|
||||
size="xs"
|
||||
shortCut={{ Icon: CornerDownLeft, hide: false }}
|
||||
on:click={() => {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
export async function openPreview(test: boolean = false) {
|
||||
if (!previewOpen) {
|
||||
previewOpen = true
|
||||
flowPreviewContent?.refresh()
|
||||
if (!test) return
|
||||
}
|
||||
flowPreviewContent?.test()
|
||||
@@ -27,6 +28,7 @@
|
||||
let flowPreviewContent: FlowPreviewContent
|
||||
let jobId: string | undefined = undefined
|
||||
let job: Job | undefined = undefined
|
||||
let preventEscape = false
|
||||
|
||||
$: upToDisabled =
|
||||
$selectedId == undefined ||
|
||||
@@ -79,6 +81,9 @@
|
||||
on:click={() => {
|
||||
previewMode = 'whole'
|
||||
previewOpen = !previewOpen
|
||||
if (previewOpen) {
|
||||
flowPreviewContent?.refresh()
|
||||
}
|
||||
}}
|
||||
startIcon={{ icon: Play }}
|
||||
id="flow-editor-test-flow"
|
||||
@@ -86,7 +91,7 @@
|
||||
Test flow
|
||||
</Button>
|
||||
|
||||
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
|
||||
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
|
||||
<FlowPreviewContent
|
||||
bind:this={flowPreviewContent}
|
||||
open={previewOpen}
|
||||
@@ -100,5 +105,6 @@
|
||||
previewOpen = false
|
||||
dispatch('openTriggers', e.detail)
|
||||
}}
|
||||
bind:preventEscape
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
export let closeButton: boolean = true
|
||||
export let displayArrow: boolean = false
|
||||
export let placement: any = 'bottom'
|
||||
export let disablePopup: boolean = false
|
||||
export let openOnHover: boolean = false
|
||||
|
||||
const {
|
||||
elements: { trigger, content, arrow, close },
|
||||
@@ -22,7 +24,14 @@
|
||||
$: sync.open(open, (v) => (open = v))
|
||||
</script>
|
||||
|
||||
<button class="w-full h-full" type="button" use:melt={$trigger} aria-label="Popup button">
|
||||
<button
|
||||
class="w-full h-full"
|
||||
type="button"
|
||||
use:melt={$trigger}
|
||||
aria-label="Popup button"
|
||||
on:mouseenter={() => (openOnHover ? (open = true) : null)}
|
||||
on:mouseleave={() => (openOnHover ? (open = false) : null)}
|
||||
>
|
||||
<slot name="trigger" />
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let dropdownItems: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
@@ -7,6 +9,8 @@
|
||||
}> = []
|
||||
|
||||
export let fullMenu: boolean = false
|
||||
export let noTrigger: boolean = false
|
||||
export let expandRight = false
|
||||
|
||||
let open = false
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
@@ -25,21 +29,31 @@
|
||||
}
|
||||
open = false
|
||||
}
|
||||
|
||||
let hasCloseButton = $$slots['close button']
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex flex-col relative !overflow-visible"
|
||||
class="flex flex-col relative !overflow-visible {expandRight ? 'pr-2 -mr-2' : ''}"
|
||||
on:mouseenter={handleMouseEnter}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
>
|
||||
<slot name="close button" />
|
||||
{#if hasCloseButton}
|
||||
<slot name="close button" />
|
||||
{:else}
|
||||
<div class="w-[31px]" />
|
||||
{/if}
|
||||
|
||||
{#if fullMenu}
|
||||
<div
|
||||
class="absolute flex-col top-[30px] left-0 z-50 bg-surface border-l border-b {open
|
||||
? 'rounded-md rounded-tl-none overflow-hidden shadow-md'
|
||||
: 'rounded-bl-md'}"
|
||||
class={twMerge(
|
||||
'absolute flex-col left-0 z-50 bg-surface border-l border-b overflow-hidden',
|
||||
hasCloseButton ? 'top-[30px]' : 'top-[1px]',
|
||||
open ? 'rounded-md rounded-tl-none shadow-md' : 'rounded-bl-md',
|
||||
hasCloseButton ? '' : 'rounded-tl-md border-t',
|
||||
noTrigger && !dropdownItems.some((item) => item.selected) ? 'rounded-md border' : ''
|
||||
)}
|
||||
>
|
||||
{#each dropdownItems as item}
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { classes } from '$lib/components/common/alert/model'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { X } from 'lucide-svelte'
|
||||
|
||||
export let inputSelected: 'history' | 'captures' | 'saved' | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="min-h-[34px]">
|
||||
<div
|
||||
class="rounded-md flex flex-row gap-2 items-center py-1 px-2 w-fit {classes['info']
|
||||
.bgClass} {inputSelected ? '' : 'hidden'}"
|
||||
>
|
||||
<p class={twMerge(classes['info'].descriptionClass, 'text-xs px-2')}>
|
||||
Using {inputSelected === 'history'
|
||||
? 'historic'
|
||||
: inputSelected === 'captures'
|
||||
? 'captures'
|
||||
: 'saved'} input arguments
|
||||
</p>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
startIcon={{ icon: X }}
|
||||
shortCut={{ key: 'esc', withoutModifier: true }}
|
||||
nonCaptureEvent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,6 +19,7 @@
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
export let diff: Record<string, SchemaDiff> = {}
|
||||
export let nestedClasses = ''
|
||||
export let isValid: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flipDurationMs = 200
|
||||
@@ -98,6 +99,7 @@
|
||||
{diff}
|
||||
{nestedParent}
|
||||
{shouldDispatchChanges}
|
||||
bind:isValid
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if !disableDnd}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
|
||||
export let payloadData: Record<string, any>
|
||||
export let payloadData: Record<string, any> | string
|
||||
export let date: string | undefined
|
||||
export let selected = false
|
||||
export let hovering = false
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<Cell
|
||||
wrap
|
||||
class="text-2xs font-normal text-left p-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
|
||||
class="text-2xs font-normal text-left py-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
|
||||
title={formatDate(date)}
|
||||
>
|
||||
{formatDateShort(date)}
|
||||
@@ -70,7 +70,7 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-xs border w-full font-normal text-left p-1 rounded-md whitespace-nowrap overflow-hidden text-ellipsis',
|
||||
'text-xs border w-full font-normal text-tertiary text-left p-1 rounded-md whitespace-nowrap overflow-hidden text-ellipsis',
|
||||
hovering && 'border-surface'
|
||||
)}
|
||||
on:click={() => {
|
||||
@@ -82,7 +82,15 @@
|
||||
{JSON.stringify(payloadData)}
|
||||
</div>
|
||||
<svelte:fragment slot="overlay">
|
||||
<ObjectViewer json={payloadData} />
|
||||
{#if payloadData === 'WINDMILL_TOO_BIG'}
|
||||
<div class="text-center text-tertiary text-xs">
|
||||
Payload too big to preview but can still be loaded
|
||||
</div>
|
||||
{:else}
|
||||
<div class="max-w-60 overflow-auto">
|
||||
<ObjectViewer json={payloadData} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
|
||||
|
||||
@@ -268,10 +268,10 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="italic text-xs"
|
||||
>An email notification will be sent to {premiumInfo.owner} if the specified threshold amount
|
||||
is reached during a given month.</p
|
||||
>
|
||||
<p class="italic text-xs">
|
||||
An email notification will be sent to {premiumInfo.owner} if the specified threshold amount
|
||||
is exceeded during a given month.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="w-full">
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
kind: 'http'
|
||||
},
|
||||
{
|
||||
label: 'Websockets',
|
||||
label: 'WebSockets',
|
||||
href: '/websocket_triggers',
|
||||
icon: Unplug,
|
||||
disabled: $userStore?.operator,
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
let tableHeight: number = 0
|
||||
const dispatch = createEventDispatcher()
|
||||
let tableContainer: HTMLDivElement
|
||||
let isLoading = false
|
||||
export let loading = false
|
||||
|
||||
setContext<DatatableContext>('datatable', {
|
||||
size
|
||||
@@ -40,23 +40,19 @@
|
||||
$: contentHeight = tableHeight - footerHeight
|
||||
|
||||
function checkScrollStatus() {
|
||||
if (!infiniteScroll || isLoading) return
|
||||
if (!infiniteScroll || loading) return
|
||||
|
||||
const hasScrollbar = tableContainer.scrollHeight > tableContainer.clientHeight
|
||||
if (!hasScrollbar && hasMore) {
|
||||
isLoading = true
|
||||
dispatch('loadMore')
|
||||
setTimeout(() => {
|
||||
isLoading = false
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!infiniteScroll || isLoading) {
|
||||
if (isLoading) {
|
||||
if (!infiniteScroll || loading) {
|
||||
if (loading) {
|
||||
const checkAgain = () => {
|
||||
if (!isLoading) {
|
||||
if (!loading) {
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
@@ -67,15 +63,11 @@
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = tableContainer
|
||||
if (scrollHeight - (scrollTop + clientHeight) < 50) {
|
||||
isLoading = true
|
||||
dispatch('loadMore')
|
||||
setTimeout(() => {
|
||||
isLoading = false
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
$: if (tableContainer && hasMore && !isLoading) {
|
||||
$: if (tableContainer && hasMore && !loading) {
|
||||
checkScrollStatus()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
export let captureTable: CaptureTable | undefined
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
captureToggle: undefined
|
||||
captureToggle: { disableOnly?: boolean }
|
||||
updateSchema: { payloadData: Record<string, any>; redirect: boolean }
|
||||
}>()
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (captureInfo.active) {
|
||||
dispatch('captureToggle')
|
||||
dispatch('captureToggle', {
|
||||
// this on destroy can be called after capturing has already been stopped (aka after on destroy of the wrapper), make sure we do not start it again
|
||||
disableOnly: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,7 +73,7 @@
|
||||
<AnimatedButton animate={captureInfo.active} baseRadius="6px" wrapperClasses="ml-[-2px]">
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => dispatch('captureToggle')}
|
||||
on:click={() => dispatch('captureToggle', {})}
|
||||
variant="border"
|
||||
{disabled}
|
||||
color="light"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import CustomPopover from '../CustomPopover.svelte'
|
||||
import { Webhook, Route, Unplug, Mail, Play } from 'lucide-svelte'
|
||||
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
|
||||
import { isObject } from '$lib/utils'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { type TriggerKind } from '../triggers'
|
||||
import { CaptureService } from '$lib/gen'
|
||||
@@ -15,9 +14,10 @@
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
|
||||
import InfiniteList from '../InfiniteList.svelte'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { isObject, sendUserToast } from '$lib/utils'
|
||||
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import type { Capture } from '$lib/gen'
|
||||
|
||||
export let path: string
|
||||
export let hasPreprocessor = false
|
||||
@@ -57,6 +57,11 @@
|
||||
testWithArgs: any
|
||||
}>()
|
||||
|
||||
interface CaptureWithPayload extends Capture {
|
||||
getFullCapture?: () => Promise<any>
|
||||
payloadData?: any
|
||||
}
|
||||
|
||||
export async function loadCaptures(refresh: boolean = false) {
|
||||
await infiniteList?.loadData(refresh ? 'refresh' : 'loadMore')
|
||||
}
|
||||
@@ -71,19 +76,36 @@
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
let capturesWithPayload = captures.map((capture) => {
|
||||
const payload = isObject(capture.payload) ? capture.payload : {}
|
||||
const triggerExtra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
|
||||
return {
|
||||
...capture,
|
||||
payloadData:
|
||||
testKind === 'preprocessor'
|
||||
? {
|
||||
...payload,
|
||||
...triggerExtra
|
||||
}
|
||||
: { ...payload }
|
||||
|
||||
let capturesWithPayload: CaptureWithPayload[] = captures.map((capture) => {
|
||||
let newCapture: CaptureWithPayload = { ...capture }
|
||||
if (capture.payload === 'WINDMILL_TOO_BIG') {
|
||||
newCapture = {
|
||||
...capture,
|
||||
getFullCapture: () =>
|
||||
CaptureService.getCapture({
|
||||
workspace: $workspaceStore!,
|
||||
id: capture.id
|
||||
})
|
||||
}
|
||||
}
|
||||
const trigger_extra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
|
||||
newCapture.payloadData =
|
||||
testKind === 'preprocessor'
|
||||
? capture.payload === 'WINDMILL_TOO_BIG'
|
||||
? {
|
||||
payload: capture.payload,
|
||||
...trigger_extra
|
||||
}
|
||||
: typeof capture.payload === 'object'
|
||||
? {
|
||||
...capture.payload,
|
||||
...trigger_extra
|
||||
}
|
||||
: trigger_extra
|
||||
: capture.payload
|
||||
|
||||
return newCapture
|
||||
})
|
||||
return capturesWithPayload
|
||||
}
|
||||
@@ -98,15 +120,33 @@
|
||||
infiniteList?.setDeleteItemFn(deleteInputFn)
|
||||
}
|
||||
|
||||
function handleSelect(capture: any) {
|
||||
async function handleSelect(capture: any) {
|
||||
if (selected === capture.id) {
|
||||
deselect()
|
||||
} else {
|
||||
const payloadData = await getPayload(capture)
|
||||
selected = capture.id
|
||||
dispatch('select', capture.payloadData)
|
||||
dispatch('select', structuredClone(payloadData))
|
||||
}
|
||||
}
|
||||
|
||||
async function getPayload(capture: any) {
|
||||
let payloadData = {}
|
||||
if (capture.getFullCapture) {
|
||||
const fullCapture = await capture.getFullCapture()
|
||||
payloadData =
|
||||
testKind === 'preprocessor'
|
||||
? {
|
||||
...(typeof fullCapture.payload === 'object' ? fullCapture.payload : {}),
|
||||
...(typeof fullCapture.trigger_extra === 'object' ? fullCapture.trigger_extra : {})
|
||||
}
|
||||
: fullCapture.payload
|
||||
} else {
|
||||
payloadData = structuredClone(capture.payloadData)
|
||||
}
|
||||
return payloadData
|
||||
}
|
||||
|
||||
function deselect() {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
@@ -209,7 +249,7 @@
|
||||
<svelte:fragment slot="columns">
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col class="w-20" />
|
||||
<col class="w-16" />
|
||||
<col />
|
||||
</colgroup>
|
||||
</svelte:fragment>
|
||||
@@ -265,9 +305,10 @@
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Use as input schema',
|
||||
onClick: () => {
|
||||
onClick: async () => {
|
||||
const payloadData = await getPayload(item)
|
||||
dispatch('updateSchema', {
|
||||
payloadData: item.payloadData,
|
||||
payloadData,
|
||||
redirect: true,
|
||||
args: true
|
||||
})
|
||||
@@ -275,20 +316,23 @@
|
||||
disabled: !isFlow || testKind !== 'main'
|
||||
}
|
||||
].filter((item) => !item.disabled)}
|
||||
on:click={() => {
|
||||
on:click={async () => {
|
||||
const payloadData = await getPayload(item)
|
||||
if (isFlow && testKind === 'main') {
|
||||
dispatch('testWithArgs', item.payloadData)
|
||||
dispatch('testWithArgs', payloadData)
|
||||
} else {
|
||||
dispatch('applyArgs', {
|
||||
kind: testKind,
|
||||
args: item.payloadData
|
||||
args: payloadData
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={testKind === 'preprocessor' && !hasPreprocessor}
|
||||
title={isFlow && testKind === 'main'
|
||||
? 'Test flow with args'
|
||||
: 'Apply args to preprocessor'}
|
||||
: testKind === 'preprocessor'
|
||||
? 'Apply args to preprocessor'
|
||||
: 'Apply args to inputs'}
|
||||
startIcon={isFlow && testKind === 'main' ? { icon: Play } : {}}
|
||||
>
|
||||
{isFlow && testKind === 'main' ? 'Test' : 'Apply args'}
|
||||
|
||||
@@ -110,12 +110,12 @@
|
||||
)
|
||||
}
|
||||
|
||||
export async function handleCapture() {
|
||||
if (!captureActive) {
|
||||
export async function handleCapture(e: CustomEvent<{ disableOnly?: boolean }>) {
|
||||
if (captureActive || e.detail.disableOnly) {
|
||||
captureActive = false
|
||||
} else {
|
||||
await setConfig()
|
||||
capture()
|
||||
} else {
|
||||
captureActive = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +178,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'webhook'}
|
||||
@@ -197,9 +195,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'http'}
|
||||
@@ -209,16 +205,15 @@
|
||||
{showCapture}
|
||||
can_write={true}
|
||||
runnableArgs={data?.args}
|
||||
bind:args
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
headless
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'email'}
|
||||
@@ -235,9 +230,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'kafka'}
|
||||
@@ -252,9 +245,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:captureToggle={handleCapture}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'nats'}
|
||||
@@ -269,9 +260,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:captureToggle={handleCapture}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
export let args: Record<string, any>
|
||||
|
||||
const kindToName: { [key: string]: string } = {
|
||||
websocket: 'Websocket',
|
||||
websocket: 'WebSocket',
|
||||
nats: 'NATS server(s)',
|
||||
kafka: 'Kafka broker(s)'
|
||||
}
|
||||
@@ -47,7 +47,6 @@
|
||||
await promise
|
||||
sendUserToast(`Successfully connected to ${kindToName[kind]}`)
|
||||
} catch (err) {
|
||||
|
||||
if (!promise?.isCancelled) {
|
||||
sendUserToast(`Error testing ${kindToName[kind]}: ${err?.body ?? 'Unknown error'}`, true)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
|
||||
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
|
||||
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">HTTP</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">WebSockets</Tab>
|
||||
<Tab value="postgres" selectedClass="text-primary text-sm font-semibold">Postgres</Tab>
|
||||
<Tab
|
||||
value="kafka"
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
|
||||
const captureTypeLabels: Record<CaptureTriggerKind, string> = {
|
||||
http: 'New custom HTTP route',
|
||||
websocket: 'New websocket trigger',
|
||||
websocket: 'New WebSocket trigger',
|
||||
webhook: 'Webhook',
|
||||
kafka: 'New kafka trigger',
|
||||
kafka: 'New Kafka trigger',
|
||||
email: 'Email trigger',
|
||||
nats: 'NATS trigger'
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
<RouteEditorConfigSection
|
||||
showCapture={false}
|
||||
can_write={true}
|
||||
bind:args
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
headless
|
||||
{isFlow}
|
||||
{path}
|
||||
|
||||
@@ -19,10 +19,9 @@
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let args: Record<string, any> = { route_path: '', http_method: 'get' }
|
||||
export let dirtyRoutePath: boolean = false
|
||||
export let route_path = ''
|
||||
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
|
||||
export let route_path: string | undefined
|
||||
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined
|
||||
export let can_write: boolean = false
|
||||
export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
|
||||
undefined
|
||||
@@ -36,12 +35,15 @@
|
||||
let validateTimeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
let routeError: string = ''
|
||||
async function validateRoute(path: string, method: typeof http_method): Promise<void> {
|
||||
async function validateRoute(
|
||||
path: string | undefined,
|
||||
method: typeof http_method
|
||||
): Promise<void> {
|
||||
if (validateTimeout) {
|
||||
clearTimeout(validateTimeout)
|
||||
}
|
||||
validateTimeout = setTimeout(async () => {
|
||||
if (!/^[\w-:]+(\/[\w-:]+)*$/.test(path)) {
|
||||
if (!path || !method || !/^[\w-:]+(\/[\w-:]+)*$/.test(path)) {
|
||||
routeError = 'Endpoint not valid'
|
||||
} else if (initialRoutePath !== path && (await routeExists(path, method))) {
|
||||
routeError = 'Endpoint already taken'
|
||||
@@ -52,7 +54,7 @@
|
||||
}, 500)
|
||||
}
|
||||
|
||||
async function routeExists(route_path: string, method: typeof http_method) {
|
||||
async function routeExists(route_path: string, method: Exclude<typeof http_method, undefined>) {
|
||||
return await HttpTriggerService.existsRoute({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
@@ -67,9 +69,9 @@
|
||||
}/${path.replaceAll('/', '.')}/${route_path}`
|
||||
|
||||
function getHttpRoute(route_path: string | undefined) {
|
||||
return `${location.origin}${base}/api/r/${
|
||||
isCloudHosted() ? $workspaceStore + '/' : ''
|
||||
}${route_path}`
|
||||
return `${location.origin}${base}/api/r/${isCloudHosted() ? $workspaceStore + '/' : ''}${
|
||||
route_path ?? ''
|
||||
}`
|
||||
}
|
||||
|
||||
$: validateRoute(route_path, http_method)
|
||||
@@ -78,13 +80,8 @@
|
||||
|
||||
$: fullRoute = getHttpRoute(route_path)
|
||||
|
||||
$: showCapture && (http_method = 'post')
|
||||
|
||||
function updateArgs(route_path: string, http_method: string) {
|
||||
args && ((args.route_path = route_path), (args.http_method = http_method))
|
||||
}
|
||||
|
||||
$: updateArgs(route_path, http_method)
|
||||
$: !http_method && (http_method = 'post')
|
||||
$: route_path === undefined && (route_path = '')
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -108,7 +105,7 @@
|
||||
<CopyableCodeBlock
|
||||
disabled={!captureInfo.active}
|
||||
code={`curl \\
|
||||
-X ${http_method.toUpperCase()} ${captureURL} \\
|
||||
-X ${(http_method ?? 'post').toUpperCase()} ${captureURL} \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '${JSON.stringify(runnableArgs ?? {}, null, 2)}'`}
|
||||
language={bash}
|
||||
|
||||
@@ -166,10 +166,6 @@
|
||||
let drawer: Drawer
|
||||
|
||||
let dirtyPath = false
|
||||
|
||||
let args: Record<string, any> = { route_path: '' }
|
||||
|
||||
$: args && (route_path = args.route_path)
|
||||
</script>
|
||||
|
||||
{#if static_asset_config}
|
||||
@@ -227,7 +223,6 @@
|
||||
isFlow={is_flow}
|
||||
{path}
|
||||
bind:route_path
|
||||
bind:args
|
||||
bind:isValid
|
||||
bind:dirtyRoutePath
|
||||
bind:http_method
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
dirtyPath = false
|
||||
await loadTrigger()
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not load kafka trigger: ${err}`, true)
|
||||
sendUserToast(`Could not load Kafka trigger: ${err}`, true)
|
||||
} finally {
|
||||
drawerLoading = false
|
||||
}
|
||||
@@ -154,9 +154,9 @@
|
||||
<DrawerContent
|
||||
title={edit
|
||||
? can_write
|
||||
? `Edit kafka trigger ${initialPath}`
|
||||
? `Edit Kafka trigger ${initialPath}`
|
||||
: `Kafka trigger ${initialPath}`
|
||||
: 'New kafka trigger'}
|
||||
: 'New Kafka trigger'}
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
@@ -173,7 +173,7 @@
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} kafka trigger ${initialPath}`)
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} Kafka trigger ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), kafka_count: kafkaTriggers?.length }
|
||||
} catch (e) {
|
||||
console.error('impossible to load kafka triggers', e)
|
||||
console.error('impossible to load Kafka triggers', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
{#if kafkaTriggers}
|
||||
<Section label="Kafka triggers">
|
||||
{#if kafkaTriggers.length == 0}
|
||||
<div class="text-xs text-secondary text-center"> No kafka triggers </div>
|
||||
<div class="text-xs text-secondary text-center"> No Kafka triggers </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)}
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
publication: publication_name
|
||||
})
|
||||
items = items.filter((item) => item != publication_name)
|
||||
table_to_track = []
|
||||
relations = []
|
||||
transaction_to_track = ['Insert', 'Update', 'Delete']
|
||||
publication_name = ''
|
||||
sendUserToast(message)
|
||||
} catch (error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user