diff --git a/.envrc b/.envrc index 1d953f4bd7..3550a30f2d 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -use nix +use flake diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 109dfe9597..14735dc246 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -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 diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a27ca5f6bd..18145886db 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8c50c9fa..dcc672fa06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/backend/.sqlx/query-1ef48cc430870ab6c062b046bdbf0a2db057141619e9818ddb904d7c242efec3.json b/backend/.sqlx/query-1ef48cc430870ab6c062b046bdbf0a2db057141619e9818ddb904d7c242efec3.json deleted file mode 100644 index 7fc5a4c430..0000000000 --- a/backend/.sqlx/query-1ef48cc430870ab6c062b046bdbf0a2db057141619e9818ddb904d7c242efec3.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4.json b/backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json similarity index 73% rename from backend/.sqlx/query-e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4.json rename to backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json index b2ff93f8d9..890a282ad1 100644 --- a/backend/.sqlx/query-e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4.json +++ b/backend/.sqlx/query-5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd.json @@ -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" } diff --git a/backend/.sqlx/query-561b7935d687f6b9f3d6488f8489f55e6737ffa2d3716f803d32f8b68cc1e915.json b/backend/.sqlx/query-63b42286804a3f3977235935d7ac200a07c5a4ad3a50051b73f382385b89466e.json similarity index 62% rename from backend/.sqlx/query-561b7935d687f6b9f3d6488f8489f55e6737ffa2d3716f803d32f8b68cc1e915.json rename to backend/.sqlx/query-63b42286804a3f3977235935d7ac200a07c5a4ad3a50051b73f382385b89466e.json index a3e2f3d338..3938de3ddf 100644 --- a/backend/.sqlx/query-561b7935d687f6b9f3d6488f8489f55e6737ffa2d3716f803d32f8b68cc1e915.json +++ b/backend/.sqlx/query-63b42286804a3f3977235935d7ac200a07c5a4ad3a50051b73f382385b89466e.json @@ -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" } diff --git a/backend/.sqlx/query-84576f6add15108ade2ed3a88185d67db17904859e5f01f1d578af4e1d062a85.json b/backend/.sqlx/query-84576f6add15108ade2ed3a88185d67db17904859e5f01f1d578af4e1d062a85.json deleted file mode 100644 index cdcf0228ca..0000000000 --- a/backend/.sqlx/query-84576f6add15108ade2ed3a88185d67db17904859e5f01f1d578af4e1d062a85.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-9a9b639611459659ae355a43f219c6da2c3d1e04d49306adaaaaf06e54ee8357.json b/backend/.sqlx/query-9a9b639611459659ae355a43f219c6da2c3d1e04d49306adaaaaf06e54ee8357.json new file mode 100644 index 0000000000..8ae3d80ca6 --- /dev/null +++ b/backend/.sqlx/query-9a9b639611459659ae355a43f219c6da2c3d1e04d49306adaaaaf06e54ee8357.json @@ -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" +} diff --git a/backend/.sqlx/query-0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664.json b/backend/.sqlx/query-d55eabd42559893bdf91c5fdab1ae58006013be26fcb9367b3c6830f38749799.json similarity index 84% rename from backend/.sqlx/query-0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664.json rename to backend/.sqlx/query-d55eabd42559893bdf91c5fdab1ae58006013be26fcb9367b3c6830f38749799.json index a7c52db593..93a338c13f 100644 --- a/backend/.sqlx/query-0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664.json +++ b/backend/.sqlx/query-d55eabd42559893bdf91c5fdab1ae58006013be26fcb9367b3c6830f38749799.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option\", envs AS \"envs: Vec\", 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\", envs AS \"envs: Vec\", 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" } diff --git a/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json b/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json new file mode 100644 index 0000000000..2ef165828e --- /dev/null +++ b/backend/.sqlx/query-e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7.json @@ -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" +} diff --git a/backend/.sqlx/query-f3d3d105b76ec2eded521c3bb3735a3765c9aa2d103e3df85c245d85ba32b3b9.json b/backend/.sqlx/query-f3d3d105b76ec2eded521c3bb3735a3765c9aa2d103e3df85c245d85ba32b3b9.json deleted file mode 100644 index 4f5d515e96..0000000000 --- a/backend/.sqlx/query-f3d3d105b76ec2eded521c3bb3735a3765c9aa2d103e3df85c245d85ba32b3b9.json +++ /dev/null @@ -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" -} diff --git a/backend/.sqlx/query-fc243af1bc70f04e28c006364d6dfdfc49981568e8c496fb6a163c499c3e4ad1.json b/backend/.sqlx/query-fc243af1bc70f04e28c006364d6dfdfc49981568e8c496fb6a163c499c3e4ad1.json new file mode 100644 index 0000000000..57ae674eb4 --- /dev/null +++ b/backend/.sqlx/query-fc243af1bc70f04e28c006364d6dfdfc49981568e8c496fb6a163c499c3e4ad1.json @@ -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" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 7be5c2248d..3237d15688 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -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", ] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index f394db0473..2961e18c9c 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -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 "] 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" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index aa561843a1..6b6909ab7b 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -35402ea9fc459245d8b734b50ed98923e3061514 \ No newline at end of file +7cc103e5827b1ec91180a5939a9c022a2a1856c6 \ No newline at end of file diff --git a/backend/migrations/20250128154615_team_last_warning_sent.down.sql b/backend/migrations/20250128154615_team_last_warning_sent.down.sql new file mode 100644 index 0000000000..76e5671782 --- /dev/null +++ b/backend/migrations/20250128154615_team_last_warning_sent.down.sql @@ -0,0 +1 @@ +ALTER TABLE cloud_workspace_settings DROP COLUMN last_warning_sent; \ No newline at end of file diff --git a/backend/migrations/20250128154615_team_last_warning_sent.up.sql b/backend/migrations/20250128154615_team_last_warning_sent.up.sql new file mode 100644 index 0000000000..8b5cbd1c8b --- /dev/null +++ b/backend/migrations/20250128154615_team_last_warning_sent.up.sql @@ -0,0 +1 @@ +ALTER TABLE cloud_workspace_settings ADD COLUMN last_warning_sent TIMESTAMP NULL; \ No newline at end of file diff --git a/backend/migrations/20250128162249_addback_oracledb.down.sql b/backend/migrations/20250128162249_addback_oracledb.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20250128162249_addback_oracledb.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20250128162249_addback_oracledb.up.sql b/backend/migrations/20250128162249_addback_oracledb.up.sql new file mode 100644 index 0000000000..c25f8568a6 --- /dev/null +++ b/backend/migrations/20250128162249_addback_oracledb.up.sql @@ -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; diff --git a/backend/migrations/20250129094837_websocket_can_return_message.down.sql b/backend/migrations/20250129094837_websocket_can_return_message.down.sql new file mode 100644 index 0000000000..02cd2ce8c2 --- /dev/null +++ b/backend/migrations/20250129094837_websocket_can_return_message.down.sql @@ -0,0 +1 @@ +ALTER TABLE websocket_trigger DROP COLUMN can_return_message; \ No newline at end of file diff --git a/backend/migrations/20250129094837_websocket_can_return_message.up.sql b/backend/migrations/20250129094837_websocket_can_return_message.up.sql new file mode 100644 index 0000000000..3f2a0b17a0 --- /dev/null +++ b/backend/migrations/20250129094837_websocket_can_return_message.up.sql @@ -0,0 +1 @@ +ALTER TABLE websocket_trigger ADD COLUMN can_return_message BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/backend/migrations/20250131115248_otel_global_settings.down.sql b/backend/migrations/20250131115248_otel_global_settings.down.sql new file mode 100644 index 0000000000..7d2a024490 --- /dev/null +++ b/backend/migrations/20250131115248_otel_global_settings.down.sql @@ -0,0 +1 @@ +DELETE FROM global_settings WHERE name = 'otel'; diff --git a/backend/migrations/20250131115248_otel_global_settings.up.sql b/backend/migrations/20250131115248_otel_global_settings.up.sql new file mode 100644 index 0000000000..b1ce615a6f --- /dev/null +++ b/backend/migrations/20250131115248_otel_global_settings.up.sql @@ -0,0 +1 @@ +INSERT INTO global_settings (name, value) VALUES ('otel', '{}'); diff --git a/backend/parsers/windmill-parser-py-imports/Cargo.toml b/backend/parsers/windmill-parser-py-imports/Cargo.toml index 078b15418b..7bc558f9c0 100644 --- a/backend/parsers/windmill-parser-py-imports/Cargo.toml +++ b/backend/parsers/windmill-parser-py-imports/Cargo.toml @@ -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 diff --git a/backend/parsers/windmill-parser-py-imports/src/lib.rs b/backend/parsers/windmill-parser-py-imports/src/lib.rs index d0a5c9622b..58f857ef08 100644 --- a/backend/parsers/windmill-parser-py-imports/src/lib.rs +++ b/backend/parsers/windmill-parser-py-imports/src/lib.rs @@ -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", diff --git a/backend/parsers/windmill-parser-wasm/flake.lock b/backend/parsers/windmill-parser-wasm/flake.lock new file mode 100644 index 0000000000..c8733c1921 --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/flake.lock @@ -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 +} diff --git a/backend/parsers/windmill-parser-wasm/flake.nix b/backend/parsers/windmill-parser-wasm/flake.nix new file mode 100644 index 0000000000..ac97d8c80a --- /dev/null +++ b/backend/parsers/windmill-parser-wasm/flake.nix @@ -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"; + }; + }); +} diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index 587a25d64a..33488c6356 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -1506,9 +1506,20 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> { async fn handle_zombie_jobs(db: &Pool, 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(); diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index a168c7dcc0..611e6426a6 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -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 diff --git a/backend/windmill-api/src/capture.rs b/backend/windmill-api/src/capture.rs index 22824791a1..fe752336f3 100644 --- a/backend/windmill-api/src/capture.rs +++ b/backend/windmill-api/src/capture.rs @@ -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, + Path((w_id, id)): Path<(String, i64)>, +) -> JsonResult { + 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, diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index b5369c0902..f039c220fb 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -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>, result: Option>, } -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, username: &str, -) -> error::Result { +) -> error::Result<(Box, 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::(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::(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, + username: &str, +) -> error::Result { + 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::(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::(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 diff --git a/backend/windmill-api/src/slack_approvals.rs b/backend/windmill-api/src/slack_approvals.rs index 9b02f4c73a..8d39131e7d 100644 --- a/backend/windmill-api/src/slack_approvals.rs +++ b/backend/windmill-api/src/slack_approvals.rs @@ -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) diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index fdb40d97c5..b603259664 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -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); diff --git a/backend/windmill-api/src/websocket_triggers.rs b/backend/windmill-api/src/websocket_triggers.rs index 7e70c602bd..f90bb8def0 100644 --- a/backend/windmill-api/src/websocket_triggers.rs +++ b/backend/windmill-api/src/websocket_triggers.rs @@ -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>, initial_messages: Option>>, url_runnable_args: Option>, + can_return_message: bool, } #[derive(Deserialize)] @@ -101,6 +104,7 @@ pub struct WebsocketTrigger { filters: Vec>>, initial_messages: Option>>>, url_runnable_args: Option>>, + can_return_message: bool, } #[derive(Deserialize)] @@ -112,6 +116,7 @@ struct EditWebsocketTrigger { filters: Vec>, initial_messages: Option>>, url_runnable_args: Option>, + 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>>, &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>, - authed: ApiAuthed, - db: &DB, - workspace_id: &str, -) -> error::Result { - let user_db = UserDB::new(db.clone()); - +) -> error::Result>> { let args = if let Some(args) = args { serde_json::from_str::>>>(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 { + 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>>, - 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 { 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::(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>, Message>, + writer: &mut SplitSink>, Message>, db: &DB, ) -> error::Result<()> { let initial_messages: Vec = 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::(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, + ) -> () { + 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, + 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::(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, +) -> 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::(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(()) diff --git a/backend/windmill-common/src/cache.rs b/backend/windmill-common/src/cache.rs index 13d71dd402..83cec0b417 100644 --- a/backend/windmill-common/src/cache.rs +++ b/backend/windmill-common/src/cache.rs @@ -529,7 +529,7 @@ pub mod script { lock AS \"lock: String\", \ language AS \"language: Option\", \ envs AS \"envs: Vec\", \ - 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 + }, }), }) }); diff --git a/backend/windmill-common/src/variables.rs b/backend/windmill-common/src/variables.rs index 7f91758c90..353b26ecb5 100644 --- a/backend/windmill-common/src/variables.rs +++ b/backend/windmill-common/src/variables.rs @@ -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 = 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 { - 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) } diff --git a/backend/windmill-worker/nsjail/run.python3.config.proto b/backend/windmill-worker/nsjail/run.python3.config.proto index a9d61dc8b1..b49b9cfbfe 100644 --- a/backend/windmill-worker/nsjail/run.python3.config.proto +++ b/backend/windmill-worker/nsjail/run.python3.config.proto @@ -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} diff --git a/backend/windmill-worker/src/common.rs b/backend/windmill-worker/src/common.rs index 43a0a27654..099300f621 100644 --- a/backend/windmill-worker/src/common.rs +++ b/backend/windmill-worker/src/common.rs @@ -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(); } diff --git a/backend/windmill-worker/src/python_executor.rs b/backend/windmill-worker/src/python_executor.rs index 001f771eb7..8c134166a0 100644 --- a/backend/windmill-worker/src/python_executor.rs +++ b/backend/windmill-worker/src/python_executor.rs @@ -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 = std::env::var("PIP_TRUSTED_HOST").ok(); - static ref PIP_INDEX_CERT: Option = std::env::var("PIP_INDEX_CERT").ok(); + static ref TRUSTED_HOST: Option = var("PY_TRUSTED_HOST").ok().or(var("PIP_TRUSTED_HOST").ok()); + static ref INDEX_CERT: Option = 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 = std::env::var("EPHEMERAL_TOKEN_CMD").ok(); + static ref EPHEMERAL_TOKEN_CMD: Option = 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); diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index b320b9a445..cf24258b9e 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -62,6 +62,8 @@ use windmill_queue::{ type DB = sqlx::Pool; +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 { diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 69d2803848..42af0ae690 100644 --- a/benchmarks/lib.ts +++ b/benchmarks/lib.ts @@ -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 { return await windmill.UserService.login({ diff --git a/cli/conf.ts b/cli/conf.ts index eab154fec9..2bb3a69b9b 100644 --- a/cli/conf.ts +++ b/cli/conf.ts @@ -22,6 +22,7 @@ export interface SyncOptions { excludes?: string[]; defaultTs?: "bun" | "deno"; codebases?: Codebase[]; + parallel?: number; } export interface Codebase { diff --git a/cli/main.ts b/cli/main.ts index 517a3edf5c..43e5f9d022 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -60,7 +60,7 @@ export { // } // }); -export const VERSION = "1.452.1"; +export const VERSION = "1.455.1"; const command = new Command() .name("wmill") diff --git a/cli/script.ts b/cli/script.ts index e6b08e15da..6ab67212e8 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -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 { + 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") diff --git a/cli/sync.ts b/cli/sync.ts index e8d6bb4d1a..b691a0d42c 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -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(); + 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 ", "Include a message that will be added to all scripts/flows/apps updated during this push" ) + .option( + "--parallel ", + "Number of changes to process in parallel" + ) // deno-lint-ignore no-explicit-any .action(push as any); diff --git a/cli/utils.ts b/cli/utils.ts index fcb07bea9e..027ff7ce00 100644 --- a/cli/utils.ts +++ b/cli/utils.ts @@ -15,7 +15,7 @@ export function deepEqual(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(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(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)) + } +} \ No newline at end of file diff --git a/docker/DockerfileFullEe b/docker/DockerfileFullEe index 9b2e1a9cf0..512dfd61bc 100644 --- a/docker/DockerfileFullEe +++ b/docker/DockerfileFullEe @@ -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 diff --git a/docker/DockerfileNsjail b/docker/DockerfileNsjail index 975092006b..76511f4c53 100644 --- a/docker/DockerfileNsjail +++ b/docker/DockerfileNsjail @@ -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 diff --git a/flake.nix b/flake.nix index 3678ed1485..219a103cd9 100644 --- a/flake.nix +++ b/flake.nix @@ -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 ]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 397390bcd1..c30f1ed0e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index b49ce5ff32..5ba2600d9c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.452.1", + "version": "1.455.1", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index fa97e1cdf2..5ebb4b6cc2 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -381,11 +381,11 @@ ? 'border-2 border-green-500 bg-surface' : '' )} + data-schema-picker > {#if diffStatus && typeof diffStatus === 'object' && diffStatus.diff !== 'same'}
-
-
- {#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'} -
- Payload too big to preview but can still be loaded
- {:else if Object.keys(selectedCapture || {}).length > 0} -
- -
- {:else} -
- Select an Input to preview scripts arguments -
- {/if} -
- - - - - -
-
+
+
{#if isRunning} - +
+ +
{:else} -
+
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel} {#if selectedJobStepType == 'single'}
{/if} -
- - {#if initialPath != ''} - - {/if} -
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow} @@ -387,17 +321,68 @@
-
+
- {#key renderCount} - - {/key} + { + selectInput(e.detail.payload, e.detail?.type) + }} + {isValid} + {jsonView} + > +
+ +
+ { + jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t')) + refresh() + }} + /> +
+
+ {#if jsonView} +
+ { + if (e.detail) { + $previewArgs = e.detail + } + }} + updateOnBlur={false} + placeholder={`Write args as JSON.

Example:

{
  "foo": "12"
}`} + /> +
+ {:else} + {#key renderCount} +
+ { + savedArgs = $previewArgs + }} + bind:isValid + /> +
+ {/key} + {/if} +
{#if jobId} diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index a102700fc2..79fafd02ce 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -821,7 +821,7 @@ As a non logged in user, you can only see jobs ran by anonymous users like you {:else if job} -
+
- + { previewOpen = false }} + bind:preventEscape /> @@ -378,6 +367,7 @@ on:schemaChange={() => { resetArgs() }} + bind:isValid >
@@ -470,9 +460,8 @@ }} > { updatePreviewSchemaAndArgs(e.detail ?? undefined) }} @@ -490,12 +479,17 @@
- { - updatePreviewSchemaAndArgs(e.detail ?? undefined) - }} - flowPath={$pathStore} - /> +
+ { + updatePreviewSchemaAndArgs(e.detail ?? undefined) + }} + isFlow={true} + headless={true} + addButton={false} + /> +
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'} { updatePreviewSchemaAndArgs(e.detail ?? undefined) }} @@ -513,6 +508,7 @@ preventEnter = e.detail }} previewArgs={previewArguments} + {isValid} /> {:else if $flowInputEditorState?.selectedTab === 'json'} @@ -522,26 +518,16 @@ }} title="Json payload" > - { 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.

Example:

{
  "foo": "12"
}'} /> {:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'} @@ -575,7 +561,7 @@ - + diff --git a/frontend/src/lib/components/meltComponents/Popover.svelte b/frontend/src/lib/components/meltComponents/Popover.svelte index ec8618582a..18122ba505 100644 --- a/frontend/src/lib/components/meltComponents/Popover.svelte +++ b/frontend/src/lib/components/meltComponents/Popover.svelte @@ -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)) - diff --git a/frontend/src/lib/components/meltComponents/SideBarTab.svelte b/frontend/src/lib/components/meltComponents/SideBarTab.svelte index 2a13ca4474..5c35d75a3b 100644 --- a/frontend/src/lib/components/meltComponents/SideBarTab.svelte +++ b/frontend/src/lib/components/meltComponents/SideBarTab.svelte @@ -1,4 +1,6 @@
- + {#if hasCloseButton} + + {:else} +
+ {/if} {#if fullMenu}
item.selected) ? 'rounded-md border' : '' + )} > {#each dropdownItems as item}
+
diff --git a/frontend/src/lib/components/schema/SchemaFormDND.svelte b/frontend/src/lib/components/schema/SchemaFormDND.svelte index c29040f75a..7fc90d6c1b 100644 --- a/frontend/src/lib/components/schema/SchemaFormDND.svelte +++ b/frontend/src/lib/components/schema/SchemaFormDND.svelte @@ -19,6 +19,7 @@ export let shouldDispatchChanges: boolean = false export let diff: Record = {} export let nestedClasses = '' + export let isValid: boolean = true const dispatch = createEventDispatcher() const flipDurationMs = 200 @@ -98,6 +99,7 @@ {diff} {nestedParent} {shouldDispatchChanges} + bind:isValid > {#if !disableDnd} diff --git a/frontend/src/lib/components/schema/SchemaPickerRow.svelte b/frontend/src/lib/components/schema/SchemaPickerRow.svelte index af8839a7fc..779032efd6 100644 --- a/frontend/src/lib/components/schema/SchemaPickerRow.svelte +++ b/frontend/src/lib/components/schema/SchemaPickerRow.svelte @@ -5,7 +5,7 @@ import { twMerge } from 'tailwind-merge' import Cell from '$lib/components/table/Cell.svelte' - export let payloadData: Record + export let payloadData: Record | string export let date: string | undefined export let selected = false export let hovering = false @@ -58,7 +58,7 @@ {formatDateShort(date)} @@ -70,7 +70,7 @@
{ @@ -82,7 +82,15 @@ {JSON.stringify(payloadData)}
- + {#if payloadData === 'WINDMILL_TOO_BIG'} +
+ Payload too big to preview but can still be loaded +
+ {:else} +
+ +
+ {/if}
diff --git a/frontend/src/lib/components/settings/PremiumInfo.svelte b/frontend/src/lib/components/settings/PremiumInfo.svelte index 9ca62aa589..55d755cf4b 100644 --- a/frontend/src/lib/components/settings/PremiumInfo.svelte +++ b/frontend/src/lib/components/settings/PremiumInfo.svelte @@ -268,10 +268,10 @@ /> {/if}
-

An email notification will be sent to {premiumInfo.owner} if the specified threshold amount - is reached during a given month.

+

+ An email notification will be sent to {premiumInfo.owner} if the specified threshold amount + is exceeded during a given month. +

{/if}
diff --git a/frontend/src/lib/components/sidebar/SidebarContent.svelte b/frontend/src/lib/components/sidebar/SidebarContent.svelte index 7edc576bc8..cf82e1aaee 100644 --- a/frontend/src/lib/components/sidebar/SidebarContent.svelte +++ b/frontend/src/lib/components/sidebar/SidebarContent.svelte @@ -95,7 +95,7 @@ kind: 'http' }, { - label: 'Websockets', + label: 'WebSockets', href: '/websocket_triggers', icon: Unplug, disabled: $userStore?.operator, diff --git a/frontend/src/lib/components/table/DataTable.svelte b/frontend/src/lib/components/table/DataTable.svelte index 7b906e8db7..cd439e0ff9 100644 --- a/frontend/src/lib/components/table/DataTable.svelte +++ b/frontend/src/lib/components/table/DataTable.svelte @@ -31,7 +31,7 @@ let tableHeight: number = 0 const dispatch = createEventDispatcher() let tableContainer: HTMLDivElement - let isLoading = false + export let loading = false setContext('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() } diff --git a/frontend/src/lib/components/triggers/CaptureSection.svelte b/frontend/src/lib/components/triggers/CaptureSection.svelte index 6e92fcd6b1..ad483181e9 100644 --- a/frontend/src/lib/components/triggers/CaptureSection.svelte +++ b/frontend/src/lib/components/triggers/CaptureSection.svelte @@ -30,7 +30,7 @@ export let captureTable: CaptureTable | undefined const dispatch = createEventDispatcher<{ - captureToggle: undefined + captureToggle: { disableOnly?: boolean } updateSchema: { payloadData: Record; 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 @@
diff --git a/frontend/src/lib/components/triggers/TestTriggerConnection.svelte b/frontend/src/lib/components/triggers/TestTriggerConnection.svelte index 8c56f3627b..511b6c3f9d 100644 --- a/frontend/src/lib/components/triggers/TestTriggerConnection.svelte +++ b/frontend/src/lib/components/triggers/TestTriggerConnection.svelte @@ -13,7 +13,7 @@ export let args: Record 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) } diff --git a/frontend/src/lib/components/triggers/TriggersEditor.svelte b/frontend/src/lib/components/triggers/TriggersEditor.svelte index 922ac7f066..29d739036b 100644 --- a/frontend/src/lib/components/triggers/TriggersEditor.svelte +++ b/frontend/src/lib/components/triggers/TriggersEditor.svelte @@ -53,7 +53,7 @@ Webhooks Schedules HTTP - Websockets + WebSockets Postgres = { 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' } diff --git a/frontend/src/lib/components/triggers/TriggersWrapper.svelte b/frontend/src/lib/components/triggers/TriggersWrapper.svelte index d937bf2384..1b958e6301 100644 --- a/frontend/src/lib/components/triggers/TriggersWrapper.svelte +++ b/frontend/src/lib/components/triggers/TriggersWrapper.svelte @@ -44,7 +44,8 @@ = { 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 { + async function validateRoute( + path: string | undefined, + method: typeof http_method + ): Promise { 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) { 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 = '')
@@ -108,7 +105,7 @@ = { route_path: '' } - - $: args && (route_path = args.route_path) {#if static_asset_config} @@ -227,7 +223,6 @@ isFlow={is_flow} {path} bind:route_path - bind:args bind:isValid bind:dirtyRoutePath bind:http_method diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte index 00883a8ff0..8aeedb77e7 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggerEditorInner.svelte @@ -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 @@ @@ -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}`) }} />
diff --git a/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte b/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte index 08ac6c9b64..5e710676d1 100644 --- a/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/kafka/KafkaTriggersPanel.svelte @@ -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}
{#if kafkaTriggers.length == 0} -
No kafka triggers
+
No Kafka triggers
{:else}
{#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)} diff --git a/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte b/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte index 86f035f691..fa267def81 100644 --- a/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte +++ b/frontend/src/lib/components/triggers/postgres/PublicationPicker.svelte @@ -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) { diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte index aac583c09d..649b7bcff1 100644 --- a/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte +++ b/frontend/src/lib/components/triggers/websocket/WebsocketEditorConfigSection.svelte @@ -94,7 +94,7 @@ bind:captureTable /> {/if} -
+
| undefined = {} + let can_return_message = false let dirtyPath = false let can_write = true let drawerLoading = true @@ -92,6 +93,7 @@ initial_messages = [] url_runnable_args = defaultValues?.url_runnable_args ?? {} dirtyPath = false + can_return_message = false } finally { drawerLoading = false } @@ -112,6 +114,7 @@ filters = s.filters initial_messages = s.initial_messages ?? [] url_runnable_args = s.url_runnable_args + can_return_message = s.can_return_message can_write = canWrite(s.path, s.extra_perms, $userStore) } @@ -168,7 +171,8 @@ url, filters, initial_messages, - url_runnable_args + url_runnable_args, + can_return_message } }) sendUserToast(`Websocket trigger ${path} updated`) @@ -183,7 +187,8 @@ enabled: true, filters, initial_messages, - url_runnable_args + url_runnable_args, + can_return_message } }) sendUserToast(`Websocket trigger ${path} created`) @@ -202,9 +207,9 @@ @@ -250,7 +255,7 @@ {#if edit} Changes can take up to 30 seconds to take effect. {:else} - New websocket triggers can take up to 30 seconds to start listening. + New WebSocket triggers can take up to 30 seconds to start listening. {/if}
@@ -277,22 +282,36 @@ bind:isValid /> -
-

- Pick a script or flow to be triggered -

-
- +
+
+

+ Pick a script or flow to be triggered +

+
+ +
+ + { + can_return_message = !can_return_message + }} + options={{ + right: 'Send runnable result', + rightTooltip: + 'Whether the runnable result should be sent as a message to the websocket server when not null.' + }} + />
diff --git a/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte b/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte index b53862a82c..7b633a1262 100644 --- a/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte +++ b/frontend/src/lib/components/triggers/websocket/WebsocketTriggersPanel.svelte @@ -64,13 +64,13 @@ {#if isCloudHosted()} - Websocket triggers are disabled in the multi-tenant cloud. + WebSocket triggers are disabled in the multi-tenant cloud. {:else}
- Websocket triggers allow real-time bidirectional communication between your scripts/flows and - external systems. Each trigger creates a unique websocket endpoint. + WebSocket triggers allow real-time bidirectional communication between your scripts/flows and + external systems. Each trigger creates a unique WebSocket endpoint. {#if !newItem} -
+
{#if wsTriggers} {#if wsTriggers.length == 0} -
No WS triggers
+
No WebSocket triggers
{:else}
{#each wsTriggers as wsTriggers (wsTriggers.path)} diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index f696088a4d..aa26013684 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -22,7 +22,7 @@ import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' import { sendUserToast } from '$lib/toast' import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte' - import SavedInputs from '$lib/components/SavedInputs.svelte' + import SavedInputsV2 from '$lib/components/SavedInputsV2.svelte' import { FolderOpen, Archive, @@ -60,10 +60,12 @@ import json from 'svelte-highlight/languages/json' import { writable } from 'svelte/store' import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte' + import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte' import WebsocketTriggersPanel from '$lib/components/triggers/websocket/WebsocketTriggersPanel.svelte' import KafkaTriggersPanel from '$lib/components/triggers/kafka/KafkaTriggersPanel.svelte' import NatsTriggersPanel from '$lib/components/triggers/nats/NatsTriggersPanel.svelte' import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte' + import Toggle from '$lib/components/Toggle.svelte' let flow: Flow | undefined let can_write = false @@ -74,6 +76,8 @@ let scheduledForStr: string | undefined = undefined let invisible_to_owner: boolean | undefined = undefined let overrideTag: string | undefined = undefined + let inputSelected: 'saved' | 'history' | undefined = undefined + let jsonView = false const triggersCount = writable(undefined) @@ -430,7 +434,7 @@
-
+
{#if !emptyString(flow?.description)} {/if} @@ -443,20 +447,41 @@ {/if} - +
+
+ + { + runForm?.setCode(JSON.stringify(args ?? {}, null, '\t')) + }} + /> +
+ + +
+
{#if !emptyString(flow.summary)} @@ -499,13 +524,14 @@
- { const nargs = JSON.parse(JSON.stringify(e.detail)) - runForm?.setArgs(nargs) args = nargs }} /> diff --git a/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte index 13c9cfd1d9..d4dba36df7 100644 --- a/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte @@ -81,7 +81,7 @@ }) } catch (err) { sendUserToast( - `Cannot ` + (enabled ? 'enable' : 'disable') + ` kafka trigger: ${err.body}`, + `Cannot ` + (enabled ? 'enable' : 'disable') + ` Kafka trigger: ${err.body}`, true ) } finally { @@ -208,223 +208,230 @@ f={(x) => (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'} /> -{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.triggers} - +{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.triggers} + {:else} - - - - + + + + - {#if isCloudHosted()} - - Kafka triggers are disabled in the multi-tenant cloud. - -
- {/if} -
-
- -
-
Filter by path of
- - - - + {#if isCloudHosted()} + + Kafka triggers are disabled in the multi-tenant cloud. + +
+ {/if} +
+
+ +
+
Filter by path of
+ + + + +
+ + +
+ {#if $userStore?.is_super_admin && $userStore.username.includes('@')} + + {:else if $userStore?.is_admin || $userStore?.is_super_admin} + + {/if} +
- + {#if loading} + {#each new Array(6) as _} + + {/each} + {:else if !triggers?.length} +
No Kafka triggers
+ {:else if items?.length} +
+ {#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, kafka_resource_path, topics, extra_perms, canWrite, marked, server_id, error, last_server_ping, enabled } (path)} + {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`} + {@const ping = last_server_ping ? new Date(last_server_ping) : undefined} + {@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000} -
- {#if $userStore?.is_super_admin && $userStore.username.includes('@')} - - {:else if $userStore?.is_admin || $userStore?.is_super_admin} - - {/if} -
-
- {#if loading} - {#each new Array(6) as _} - - {/each} - {:else if !triggers?.length} -
No kafka triggers
- {:else if items?.length} -
- {#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, is_flow, kafka_resource_path, topics, extra_perms, canWrite, marked, server_id, error, last_server_ping, enabled } (path)} - {@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`} - {@const ping = last_server_ping ? new Date(last_server_ping) : undefined} - {@const pinging = ping && ping.getTime() > new Date().getTime() - 15 * 1000} - -
-
- + > +
+ - kafkaTriggerEditor?.openEdit(path, is_flow)} - class="min-w-0 grow hover:underline decoration-gray-400" - > -
- {#if marked} - - {@html marked} - - {:else} - {kafka_resource_path} - {topics.join(', ')} + kafkaTriggerEditor?.openEdit(path, is_flow)} + class="min-w-0 grow hover:underline decoration-gray-400" + > +
+ {#if marked} + + {@html marked} + + {:else} + {kafka_resource_path} - {topics.join(', ')} + {/if} +
+
+ {path} +
+
+ runnable: {script_path} +
+
+ + + +
+ {#if (enabled && (!pinging || error)) || (!enabled && error) || (enabled && !server_id)} + + + + + +
+ {#if enabled} + {#if !server_id} + Consumer is starting... + {:else} + Consumer is not connected{error ? ': ' + error : ''} + {/if} + {:else} + Consumer was disabled because of an error: {error} + {/if} +
+
+ {:else if enabled} + + + + +
Consumer is connected
+
{/if}
-
- {path} -
-
- runnable: {script_path} -
- - - -
- {#if (enabled && (!pinging || error)) || (!enabled && error) || (enabled && !server_id)} - - - - - -
- {#if enabled} - {#if !server_id} - Consumer is starting... - {:else} - Consumer is not connected{error ? ': ' + error : ''} - {/if} - {:else} - Consumer was disabled because of an error: {error} - {/if} -
-
- {:else if enabled} - - - - -
Consumer is connected
-
- {/if} -
- - { - setTriggerEnabled(path, e.detail) - }} - /> - -
- - { - goto(href) - } - }, - { - displayName: 'Delete', - type: 'delete', - icon: Trash, - disabled: !canWrite, - action: async () => { - await KafkaTriggerService.deleteKafkaTrigger({ - workspace: $workspaceStore ?? '', - path - }) - loadTriggers() - } - }, - { - displayName: canWrite ? 'Edit' : 'View', - icon: canWrite ? Pen : Eye, - action: () => { - kafkaTriggerEditor?.openEdit(path, is_flow) - } - }, - { - displayName: 'Audit logs', - icon: Eye, - href: `${base}/audit_logs?resource=${path}` - }, - { - displayName: canWrite ? 'Share' : 'See Permissions', - icon: Share, - action: () => { - shareModal.openDrawer(path, 'kafka_trigger') - } - } - ]} + { + setTriggerEnabled(path, e.detail) + }} /> + +
+ + { + goto(href) + } + }, + { + displayName: 'Delete', + type: 'delete', + icon: Trash, + disabled: !canWrite, + action: async () => { + await KafkaTriggerService.deleteKafkaTrigger({ + workspace: $workspaceStore ?? '', + path + }) + loadTriggers() + } + }, + { + displayName: canWrite ? 'Edit' : 'View', + icon: canWrite ? Pen : Eye, + action: () => { + kafkaTriggerEditor?.openEdit(path, is_flow) + } + }, + { + displayName: 'Audit logs', + icon: Eye, + href: `${base}/audit_logs?resource=${path}` + }, + { + displayName: canWrite ? 'Share' : 'See Permissions', + icon: Share, + action: () => { + shareModal.openDrawer(path, 'kafka_trigger') + } + } + ]} + /> +
-
-
-
edited by {edited_by}
the {displayDate(edited_at)}
+
edited by {edited_by}
the {displayDate(edited_at)}
-
- {/each} -
- {:else} - + > +
+ {/each} +
+ {:else} + + {/if} +
+ {#if items && items?.length > 15 && nbDisplayed < items.length} + {nbDisplayed} items out of {items.length} + {/if} -
- {#if items && items?.length > 15 && nbDisplayed < items.length} - {nbDisplayed} items out of {items.length} - - {/if} - + {/if}
-
+
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
{#if script.lock_error_logs} @@ -651,21 +650,41 @@ {/if} - +
+
+ + { + runForm?.setCode(JSON.stringify(args ?? {}, null, '\t')) + }} + /> +
+ + +
{#if !emptyString(script.summary)} @@ -695,14 +714,15 @@ {#if args} - { const nargs = JSON.parse(JSON.stringify(e.detail)) - runForm?.setArgs(nargs) args = nargs }} /> @@ -737,6 +757,7 @@
+
diff --git a/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte b/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte index fbb18311a8..794c22e48e 100644 --- a/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/user/(user)/login/+page.svelte @@ -12,6 +12,7 @@ import { clearStores } from '$lib/storeUtils' import { setLicense } from '$lib/enterpriseUtils' import Login from '$lib/components/Login.svelte' + import { onMount } from 'svelte' const email = $page.url.searchParams.get('email') ?? '' const password = $page.url.searchParams.get('password') ?? '' @@ -21,6 +22,26 @@ let showPassword = false let firstTime = false + function clearWindmillCloudCookies() { + const domain = window.location.hostname + // Check if the domain ends with ".windmill.dev" but is NOT "app.windmill.dev" + if ( + domain.endsWith('.windmill.dev') && + domain !== 'app.windmill.dev' && + domain !== 'internal.windmill.dev' + ) { + // Remove the "token" cookie for the current domain and its parent domain + document.cookie = `token=; domain=.windmill.dev; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; Secure; SameSite=None` + document.cookie = `token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; Secure; SameSite=None` + + console.log('Token cookie removed for windmill cloud instance.') + } + } + + onMount(() => { + clearWindmillCloudCookies() + }) + async function redirectUser() { if (rd?.startsWith('http')) { window.location.href = rd @@ -80,7 +101,6 @@ } try { - setLicense() redirectIfNecessary() checkFirstTimeSetup() diff --git a/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.js b/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.js index 8e3a86c5f4..4884db7f05 100644 --- a/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.js +++ b/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.js @@ -1,5 +1,5 @@ export function load() { return { - stuff: { title: 'WS triggers' } + stuff: { title: 'WebSocket triggers' } } } diff --git a/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.svelte index 76d2a34da3..67ce9c8eb6 100644 --- a/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/websocket_triggers/+page.svelte @@ -209,21 +209,21 @@ {#if isCloudHosted()} - Websocket triggers are disabled in the multi-tenant cloud. + WebSocket triggers are disabled in the multi-tenant cloud.
{/if} @@ -314,12 +314,12 @@
{#if enabled} {#if !server_id} - Websocket is starting... + WebSocket is starting... {:else} - Websocket is not connected{error ? ': ' + error : ''} + WebSocket is not connected{error ? ': ' + error : ''} {/if} {:else} - Websocket was disabled because of an error: {error} + WebSocket was disabled because of an error: {error} {/if}
@@ -329,7 +329,7 @@
- Websocket is connected{!server_id ? ' (shutting down...)' : ''} + WebSocket is connected{!server_id ? ' (shutting down...)' : ''}
{/if} diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index cfa105bbb1..5c5e8827ff 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -397,7 +397,7 @@ webhook = settings.webhook aiResourceInitialPath = settings.ai_resource?.path aiResourceInitialProvider = settings.ai_resource?.provider - selected = aiResourceInitialProvider as AiProviderTypes ?? 'openai' + selected = (aiResourceInitialProvider as AiProviderTypes) ?? 'openai' errorHandlerItemKind = settings.error_handler?.split('/')[0] as 'flow' | 'script' errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/') errorHandlerInitialScriptPath = errorHandlerScriptPath @@ -616,6 +616,14 @@ window.location.reload() }, 3000) } + + function updateFromSearchTab(searchTab: string | null) { + if (searchTab && searchTab !== tab) { + tab = searchTab as typeof tab + } + } + + $: updateFromSearchTab($page.url.searchParams.get('tab')) @@ -699,7 +707,8 @@ Link this Workspace to another Staging / Prod Workspace
- Connecting this workspace with another staging/production workspace enables web-based deployment to that workspace. + Connecting this workspace with another staging/production workspace enables web-based + deployment to that workspace.
@@ -842,12 +851,11 @@ {#if $superadmin}

- When deleting the workspace, it will be archived for a short period of time and then permanently deleted. + When deleting the workspace, it will be archived for a short period of time and then + permanently deleted.

{:else} -

- Only instance superadmins can delete a workspace. -

+

Only instance superadmins can delete a workspace.

{/if} {#if $workspaceStore === 'admins' || $workspaceStore === 'starter'}

@@ -894,7 +902,9 @@

Workspace Webhook
- + Connect your Windmill workspace to an external service to sync or get notified about any change. @@ -925,7 +935,9 @@
Workspace Error Handler
- + Define a script or flow to be executed automatically in case of error in the workspace.
@@ -996,7 +1008,8 @@
Workspace Critical Alerts
- Critical alerts within the scope of a workspace are sent to the workspace admins through a UI notification. + Critical alerts within the scope of a workspace are sent to the workspace admins through + a UI notification.
@@ -1735,10 +1754,15 @@ git push
Workspace Secret Encryption
- When updating the encryption key of a workspace, all secrets will be re-encrypted with the new key and the previous key will be replaced by the new one. + When updating the encryption key of a workspace, all secrets will be re-encrypted with + the new key and the previous key will be replaced by the new one. - - If you're manually updating the key to match another workspace key from another Windmill instance, make sure not to use the 'SECRET_SALT' environment variable or, if you're using it, make sure it the salt matches across both instances. + + If you're manually updating the key to match another workspace key from another Windmill + instance, make sure not to use the 'SECRET_SALT' environment variable or, if you're + using it, make sure it the salt matches across both instances.
diff --git a/lsp/Pipfile b/lsp/Pipfile index e13ef0ae0a..71d4e746f5 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.452.1" -wmill_pg = ">=1.452.1" +wmill = ">=1.455.1" +wmill_pg = ">=1.455.1" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 0422d293d1..76bfacf15e 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.452.1 + version: 1.455.1 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index 110ac46761..573e07b7b3 100644 --- a/powershell-client/WindmillClient/WindmillClient.psd1 +++ b/powershell-client/WindmillClient/WindmillClient.psd1 @@ -12,7 +12,7 @@ RootModule = 'WindmillClient.psm1' # Version number of this module. - ModuleVersion = '1.452.1' + ModuleVersion = '1.455.1' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index f23f33025a..a15973feb5 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.452.1" +version = "1.455.1" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index 38e48f6ec5..23211218b3 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.452.1" +version = "1.455.1" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 79ac12c49e..0000000000 --- a/shell.nix +++ /dev/null @@ -1,95 +0,0 @@ -{ pkgs ? import { } }: - -/* based on - https://discourse.nixos.org/t/how-can-i-set-up-my-rust-programming-environment/4501/9 -*/ -let - rust_overlay = import (builtins.fetchTarball - "https://github.com/oxalica/rust-overlay/archive/master.tar.gz"); - pkgs = import { overlays = [ rust_overlay ]; }; - lib = pkgs.lib; - stdenv = pkgs.stdenv; - # TODO: Pin version? - # rustVersion = "latest"; - rustVersion = "2024-09-30"; - rust = pkgs.rust-bin.nightly.${rustVersion}.default.override { - extensions = [ - "rust-src" # for rust-analyzer - "rust-analyzer" - ]; - }; -in pkgs.mkShell { - - packages = with pkgs; [ - rust - # rustup - cargo-watch - typescript # tsc - typescript-language-server - postgresql - watchexec # used in client's dev.nu - poetry # for python client - # uv - python312Packages.pip-tools # pip-compile - ]; - - # buildInputs = with pkgs; [ xz lzma ]; - - # Add the following lines to set the LD_LIBRARY_PATH - LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (with pkgs; [ - xz - libseccomp - bzip2 - openssl_3_3 - # - ])}"; - - REMOTE = "http://127.0.0.1:8000"; - REMOTE_LSP = "http://127.0.0.1:3001"; - - DATABASE_URL = - "postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable"; - - RUSTC_LINKER = "${pkgs.clang}/bin/clang"; - CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "${pkgs.clang}/bin/clang"; - - RUSTFLAGS = - "-C link-arg=-fuse-ld=${pkgs.mold}/bin/mold -Zshare-generics=y -Z threads=4"; - RUSTC_WRAPPER = "${pkgs.sccache}/bin/sccache"; - - # Useful for development - RUST_LOG = "debug"; - - # ---- Samael ---- - # https://github.com/njaremko/samael/blob/master/flake.nix#L104-L119 - # Otherwise samael crate will fail to build - # Need to tell bindgen where to find libclang - LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - - # Set C flags for Rust's bindgen program. Unlike ordinary C - # compilation, bindgen does not invoke $CC directly. Instead it - # uses LLVM's libclang. To make sure all necessary flags are - # included we need to look in a few places. - # See https://web.archive.org/web/20220523141208/https://hoverbear.org/blog/rust-bindgen-in-nix/ - BINDGEN_EXTRA_CLANG_ARGS = - "${builtins.readFile "${stdenv.cc}/nix-support/libc-crt1-cflags"} ${ - builtins.readFile "${stdenv.cc}/nix-support/libc-cflags" - } ${builtins.readFile "${stdenv.cc}/nix-support/cc-cflags"} ${ - builtins.readFile "${stdenv.cc}/nix-support/libcxx-cxxflags" - } -idirafter ${pkgs.libiconv}/include ${ - lib.optionalString stdenv.cc.isClang - "-idirafter ${stdenv.cc.cc}/lib/clang/${ - lib.getVersion stdenv.cc.cc - }/include" - } ${ - lib.optionalString stdenv.cc.isGNU - "-isystem ${stdenv.cc.cc}/include/c++/${ - lib.getVersion stdenv.cc.cc - } -isystem ${stdenv.cc.cc}/include/c++/${ - lib.getVersion stdenv.cc.cc - }/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${ - lib.getVersion stdenv.cc.cc - }/include" - }"; - # ---- Samael ---- -} diff --git a/typescript-client/client.ts b/typescript-client/client.ts index e6e7c8584e..37fd4c9df6 100644 --- a/typescript-client/client.ts +++ b/typescript-client/client.ts @@ -985,30 +985,21 @@ export async function requestInteractiveSlackApproval({ } async function getMockedApi(): Promise { - const mockedPath = getEnv("WM_MOCKED_API_FILE"); - if (mockedApi) { return mockedApi; } + const mockedPath = getEnv("WM_MOCKED_API_FILE"); + if (mockedPath) { console.info("Using mocked API from", mockedPath); + } else { + return undefined; } - const path = getEnv("WM_MOCKED_API_FILE"); - if (!path) { - console.warn( - "No mocked API file path provided at env variable WM_MOCKED_API_FILE. Using empty mocked API." - ); - mockedApi = { - variables: {}, - resources: {}, - }; - return mockedApi; - } try { const fs = await import("node:fs/promises"); - const file = await fs.readFile(path, "utf-8"); + const file = await fs.readFile(mockedPath, "utf-8"); try { mockedApi = JSON.parse(file) as MockedApi; if (!mockedApi.variables) { @@ -1019,12 +1010,20 @@ async function getMockedApi(): Promise { } return mockedApi; } catch { - console.warn("Error parsing mocked API file at path", path); - return undefined; + console.warn("Error parsing mocked API file at path", mockedPath); } } catch { - console.warn("Error reading mocked API file at path", path); - return undefined; + console.warn("Error reading mocked API file at path", mockedPath); + } + if (!mockedApi) { + console.warn( + "No mocked API file path provided at env variable WM_MOCKED_API_FILE. Using empty mocked API." + ); + mockedApi = { + variables: {}, + resources: {}, + }; + return mockedApi; } } diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index d0899729df..10934ef466 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.452.1", + "version": "1.455.1", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index a9f24d9413..51de2bf61f 100644 --- a/typescript-client/package.json +++ b/typescript-client/package.json @@ -1,7 +1,7 @@ { "name": "windmill-client", "description": "Windmill SDK client for browsers and Node.js", - "version": "1.452.1", + "version": "1.455.1", "author": "Ruben Fiszel", "license": "Apache 2.0", "devDependencies": { diff --git a/version.txt b/version.txt index 4c16fe9baf..3ec77b5d9c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.452.1 +1.455.1