diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b0a05c89..efd125ea43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [1.575.4](https://github.com/windmill-labs/windmill/compare/v1.575.3...v1.575.4) (2025-11-13) + + +### Bug Fixes + +* fix cancel job selection ([d4340b8](https://github.com/windmill-labs/windmill/commit/d4340b83ac94a9dd9febdcfa11288f35c0a57799)) + +## [1.575.3](https://github.com/windmill-labs/windmill/compare/v1.575.2...v1.575.3) (2025-11-13) + + +### Bug Fixes + +* fix cancel job selection ([fbab86c](https://github.com/windmill-labs/windmill/commit/fbab86cc43dae17727a3aa99abc43a57c6b59b57)) + +## [1.575.2](https://github.com/windmill-labs/windmill/compare/v1.575.1...v1.575.2) (2025-11-13) + + +### Bug Fixes + +* fix cancel job selection ([1c0f80d](https://github.com/windmill-labs/windmill/commit/1c0f80dc5594146b0ec18caf3b49c036695ce759)) + +## [1.575.1](https://github.com/windmill-labs/windmill/compare/v1.575.0...v1.575.1) (2025-11-13) + + +### Bug Fixes + +* Fix flow buttons clipped in Safari ([#7126](https://github.com/windmill-labs/windmill/issues/7126)) ([77316cb](https://github.com/windmill-labs/windmill/commit/77316cbd0e3ee81fccee9680541f9b2346c8a209)) +* handle string in input transform strings ([2ee17ae](https://github.com/windmill-labs/windmill/commit/2ee17aea8b29d99c5d2bb609b2becad354835de7)) +* send alternatives on timeout toast ([#6920](https://github.com/windmill-labs/windmill/issues/6920)) ([6fac896](https://github.com/windmill-labs/windmill/commit/6fac8963909fb40726d85f5e897398d2a691edb0)) + ## [1.575.0](https://github.com/windmill-labs/windmill/compare/v1.574.3...v1.575.0) (2025-11-12) diff --git a/backend/.sqlx/query-24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd.json b/backend/.sqlx/query-24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd.json deleted file mode 100644 index 7acbf9deec..0000000000 --- a/backend/.sqlx/query-24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT j.id AS \"id!\" FROM v2_job j WHERE j.id = ANY($1) AND j.trigger_kind != 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id!", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": [ - "UuidArray", - "TextArray" - ] - }, - "nullable": [ - false - ] - }, - "hash": "24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd" -} diff --git a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json index 713ccb9dd3..36ddb8ab9f 100644 --- a/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json +++ b/backend/.sqlx/query-5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55.json @@ -15,7 +15,7 @@ ] }, "nullable": [ - null + true ] }, "hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55" diff --git a/backend/.sqlx/query-78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131.json b/backend/.sqlx/query-78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131.json new file mode 100644 index 0000000000..94c81eb5b5 --- /dev/null +++ b/backend/.sqlx/query-78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "UuidArray", + "TextArray" + ] + }, + "nullable": [ + false + ] + }, + "hash": "78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131" +} diff --git a/backend/.sqlx/query-7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e.json b/backend/.sqlx/query-7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e.json new file mode 100644 index 0000000000..4311abf5ff --- /dev/null +++ b/backend/.sqlx/query-7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e" +} diff --git a/backend/.sqlx/query-a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26.json b/backend/.sqlx/query-a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26.json new file mode 100644 index 0000000000..cead5d7019 --- /dev/null +++ b/backend/.sqlx/query-a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26.json @@ -0,0 +1,66 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return, \n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, \n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, \n flow.tag, \n flow.dedicated_worker, \n flow.on_behalf_of_email, \n flow.edited_by\n FROM \n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE \n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "version", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "early_return", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "has_preprocessor", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "chat_input_enabled", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "tag", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "dedicated_worker", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "on_behalf_of_email", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "edited_by", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Int8" + ] + }, + "nullable": [ + false, + null, + null, + null, + true, + true, + true, + false + ] + }, + "hash": "a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26" +} diff --git a/backend/.sqlx/query-b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27.json b/backend/.sqlx/query-b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27.json new file mode 100644 index 0000000000..64b774cbd9 --- /dev/null +++ b/backend/.sqlx/query-b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27" +} diff --git a/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json b/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json deleted file mode 100644 index da6ca296b1..0000000000 --- a/backend/.sqlx/query-e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "tag", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "dedicated_worker", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "early_return", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "has_preprocessor", - "type_info": "Bool" - }, - { - "ordinal": 4, - "name": "chat_input_enabled", - "type_info": "Bool" - }, - { - "ordinal": 5, - "name": "on_behalf_of_email", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "edited_by", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "version", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Int8" - ] - }, - "nullable": [ - true, - true, - null, - null, - null, - true, - false, - false - ] - }, - "hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945" -} diff --git a/backend/.sqlx/query-e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d.json b/backend/.sqlx/query-f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5.json similarity index 69% rename from backend/.sqlx/query-e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d.json rename to backend/.sqlx/query-f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5.json index c7c7214a87..19842dda7d 100644 --- a/backend/.sqlx/query-e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d.json +++ b/backend/.sqlx/query-f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n path = $1\n AND workspace_id = $2\n ORDER BY created_at DESC\n LIMIT 1", + "query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n id = $1\n AND workspace_id = $2", "describe": { "columns": [ { @@ -16,7 +16,7 @@ ], "parameters": { "Left": [ - "Text", + "Int8", "Text" ] }, @@ -25,5 +25,5 @@ true ] }, - "hash": "e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d" + "hash": "f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5" } diff --git a/backend/.sqlx/query-fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca.json b/backend/.sqlx/query-fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca.json new file mode 100644 index 0000000000..b699cc582a --- /dev/null +++ b/backend/.sqlx/query-fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index d224f52b22..6452d027a9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -800,9 +800,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" dependencies = [ "aws-lc-sys", "zeroize", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.3" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" dependencies = [ "bindgen 0.72.1", "cc", @@ -6739,9 +6739,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -10933,9 +10933,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest 0.10.7", @@ -15148,7 +15148,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "aws-sdk-config", @@ -15208,7 +15208,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "argon2", @@ -15269,6 +15269,7 @@ dependencies = [ "pg_escape", "pin-project", "postgres-native-tls 0.5.0", + "postgres-native-tls 0.5.1", "prometheus", "quick_cache", "rand 0.9.0", @@ -15328,7 +15329,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.575.0" +version = "1.575.4" dependencies = [ "base64 0.22.1", "chrono", @@ -15343,7 +15344,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.575.0" +version = "1.575.4" dependencies = [ "chrono", "lazy_static", @@ -15357,7 +15358,7 @@ dependencies = [ [[package]] name = "windmill-autoscaling" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "axum", @@ -15376,7 +15377,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "async-recursion", @@ -15461,7 +15462,7 @@ dependencies = [ [[package]] name = "windmill-git-sync" -version = "1.575.0" +version = "1.575.4" dependencies = [ "regex", "serde", @@ -15476,7 +15477,7 @@ dependencies = [ [[package]] name = "windmill-indexer" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "bytes", @@ -15500,7 +15501,7 @@ dependencies = [ [[package]] name = "windmill-macros" -version = "1.575.0" +version = "1.575.4" dependencies = [ "itertools 0.14.0", "lazy_static", @@ -15512,7 +15513,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.575.0" +version = "1.575.4" dependencies = [ "convert_case 0.6.0", "serde", @@ -15521,7 +15522,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "lazy_static", @@ -15533,7 +15534,7 @@ dependencies = [ [[package]] name = "windmill-parser-csharp" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "serde_json", @@ -15545,7 +15546,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "gosyn", @@ -15557,7 +15558,7 @@ dependencies = [ [[package]] name = "windmill-parser-graphql" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "lazy_static", @@ -15569,7 +15570,7 @@ dependencies = [ [[package]] name = "windmill-parser-java" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "serde_json", @@ -15581,7 +15582,7 @@ dependencies = [ [[package]] name = "windmill-parser-nu" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "nu-parser", @@ -15592,7 +15593,7 @@ dependencies = [ [[package]] name = "windmill-parser-php" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15603,7 +15604,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "itertools 0.14.0", @@ -15615,7 +15616,7 @@ dependencies = [ [[package]] name = "windmill-parser-py-imports" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "async-recursion", @@ -15638,7 +15639,7 @@ dependencies = [ [[package]] name = "windmill-parser-ruby" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "lazy_static", @@ -15652,7 +15653,7 @@ dependencies = [ [[package]] name = "windmill-parser-rust" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -15669,7 +15670,7 @@ dependencies = [ [[package]] name = "windmill-parser-sql" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "lazy_static", @@ -15683,7 +15684,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "lazy_static", @@ -15701,7 +15702,7 @@ dependencies = [ [[package]] name = "windmill-parser-yaml" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "serde", @@ -15712,7 +15713,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "async-recursion", @@ -15747,7 +15748,7 @@ dependencies = [ [[package]] name = "windmill-sql-datatype-parser-wasm" -version = "1.575.0" +version = "1.575.4" dependencies = [ "wasm-bindgen", "wasm-bindgen-test", @@ -15757,7 +15758,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.575.0" +version = "1.575.4" dependencies = [ "anyhow", "async-once-cell", @@ -16051,13 +16052,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5702c3a8f5..457c9f91dd 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.575.0" +version = "1.575.4" authors.workspace = true edition.workspace = true @@ -33,7 +33,7 @@ members = [ exclude = ["./windmill-duckdb-ffi-internal"] [workspace.package] -version = "1.575.0" +version = "1.575.4" authors = ["Ruben Fiszel "] edition = "2021" @@ -259,7 +259,7 @@ tokio-tar = "^0" tempfile = "^3" tokio-util = { version = "^0", features = ["io"] } json-pointer = "^0" -itertools = "^0" +itertools = "^0.14.0" regex = "^1" semver = "^1" aws-sigv4 = "^1.3.4" @@ -300,7 +300,7 @@ swc_ecma_visit = "=0.104.8" async-recursion = "^1" -base64 = "^0" +base64 = "^0.22.1" base32 = "^0" hmac = "0.12.1" sha2 = "0.10.6" diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 0da534da09..f6823b4692 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -7cdb62d968feef22046b12a1217b711af457cced \ No newline at end of file +31ee9d3449f05cd0328c0fcf43e2b161dee767b9 \ No newline at end of file diff --git a/backend/src/ee_oss.rs b/backend/src/ee_oss.rs index 4791a243a3..2aefaf7431 100644 --- a/backend/src/ee_oss.rs +++ b/backend/src/ee_oss.rs @@ -3,7 +3,7 @@ pub use crate::ee::*; #[cfg(not(feature = "private"))] -pub async fn set_license_key(_license_key: String) -> () { +pub async fn set_license_key(_license_key: String, _db: Option<&windmill_common::db::DB>) -> () { // Implementation is not open source } diff --git a/backend/src/monitor.rs b/backend/src/monitor.rs index b0818c96cd..eaf83c2e4c 100644 --- a/backend/src/monitor.rs +++ b/backend/src/monitor.rs @@ -79,7 +79,7 @@ use windmill_common::{ OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS, }; use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING}; -use windmill_queue::{SameWorkerPayload, cancel_job, get_queued_job_v2}; +use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload}; use windmill_worker::{ handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES, INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN, @@ -1271,7 +1271,7 @@ pub async fn reload_license_key(conn: &Connection) -> anyhow::Result<()> { tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q); } }; - set_license_key(value).await; + set_license_key(value, conn.as_sql()).await; Ok(()) } @@ -2219,7 +2219,6 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n ); } - timeouts }; @@ -2253,7 +2252,6 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n } } - let timeouts = non_restartable_jobs .into_iter() .map(|x| (x, ErrorMessage::RestartDisabled)) @@ -2289,59 +2287,59 @@ async fn handle_zombie_jobs(db: &Pool, base_internal_url: &str, node_n continue; } if let Some(job) = job.unwrap() { - let label = if job.permissioned_as != format!("u/{}", job.created_by) - && job.permissioned_as != job.created_by - { - format!("ephemeral-script-end-user-{}", job.created_by) - } else { - "ephemeral-script".to_string() - }; - let token = create_token_for_owner( - &db, - &job.workspace_id, - &job.permissioned_as, - &label, - *SCRIPT_TOKEN_EXPIRY, - &job.permissioned_as_email, - &job.id, - None, - Some(format!("handle_zombie_jobs")), - ) - .await - .expect("could not create job token"); + let label = if job.permissioned_as != format!("u/{}", job.created_by) + && job.permissioned_as != job.created_by + { + format!("ephemeral-script-end-user-{}", job.created_by) + } else { + "ephemeral-script".to_string() + }; + let token = create_token_for_owner( + &db, + &job.workspace_id, + &job.permissioned_as, + &label, + *SCRIPT_TOKEN_EXPIRY, + &job.permissioned_as_email, + &job.id, + None, + Some(format!("handle_zombie_jobs")), + ) + .await + .expect("could not create job token"); - let client = AuthedClient::new( - base_internal_url.to_string(), - job.workspace_id.to_string(), - token, - None, - ); + let client = AuthedClient::new( + base_internal_url.to_string(), + job.workspace_id.to_string(), + token, + None, + ); - let error_message = format!( + let error_message = format!( "Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.", job.last_ping.unwrap_or_default(), *ZOMBIE_JOB_TIMEOUT, error_kind.to_string(), job.worker.clone().unwrap_or_default(), ); - let memory_peak = job.memory_peak.unwrap_or(0); - let _ = handle_job_error( - db, - &client, - &windmill_queue::MiniCompletedJob::from(job), - memory_peak, - None, - error::Error::ExecutionErr(error_message), - true, - Some(&same_worker_tx_never_used), - "", - node_name, - send_result_never_used, - #[cfg(feature = "benchmark")] - &mut windmill_common::bench::BenchmarkIter::new(), - ) - .await; - } + let memory_peak = job.memory_peak.unwrap_or(0); + let _ = handle_job_error( + db, + &client, + &windmill_queue::MiniCompletedJob::from(job), + memory_peak, + None, + error::Error::ExecutionErr(error_message), + true, + Some(&same_worker_tx_never_used), + "", + node_name, + send_result_never_used, + #[cfg(feature = "benchmark")] + &mut windmill_common::bench::BenchmarkIter::new(), + ) + .await; + } } } diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index 40a9980a96..129fd86d63 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -208,6 +208,7 @@ impl RunJob { false, None, debounce_job_id_o, + None, ) .await .expect("push has to succeed"); diff --git a/backend/tests/relative_imports.rs b/backend/tests/relative_imports.rs index 2de18732b4..052f5a3743 100644 --- a/backend/tests/relative_imports.rs +++ b/backend/tests/relative_imports.rs @@ -421,14 +421,16 @@ def main(): path: "f/rel/root_flow_renamed".into(), summary: "".into(), description: None, - value: to_raw_value(&serde_json::from_str::( - &serde_json::to_string(flow.value()) - .unwrap() - .replace("nstep1", "Foxes") - .replace("nstep2_2", "like") - .replace("nstep_4_1", "Emeralds"), - ) - .unwrap()), + value: to_raw_value( + &serde_json::from_str::( + &serde_json::to_string(flow.value()) + .unwrap() + .replace("nstep1", "Foxes") + .replace("nstep2_2", "like") + .replace("nstep_4_1", "Emeralds"), + ) + .unwrap(), + ), schema: None, draft_only: None, tag: None, @@ -437,7 +439,7 @@ def main(): deployment_message: None, visible_to_runner_only: None, on_behalf_of_email: None, - ws_error_handler_muted: None + ws_error_handler_muted: None, }) .send() .await @@ -862,6 +864,7 @@ def main(): false, None, None, + None, ) .await .unwrap(); @@ -1021,6 +1024,7 @@ def main(): false, None, debounce_job_id_o, + None, ) .await .unwrap(); @@ -1198,6 +1202,7 @@ def main(): false, None, debounce_job_id_o, + None, ) .await .unwrap(); @@ -1705,6 +1710,7 @@ WHERE false, None, None, + None, ) .await .unwrap(); @@ -1844,6 +1850,7 @@ WHERE false, None, None, + None, ) .await .unwrap(); @@ -2274,6 +2281,7 @@ WHERE false, None, None, + None, ) .await .unwrap(); @@ -2399,6 +2407,7 @@ WHERE // false, // None, // None, + // None, // ) // .await // .unwrap(); diff --git a/backend/tests/worker.rs b/backend/tests/worker.rs index 72b6aeea52..57d7833c9d 100644 --- a/backend/tests/worker.rs +++ b/backend/tests/worker.rs @@ -2769,7 +2769,7 @@ async fn test_result_format(db: Pool) -> anyhow::Result<()> { let response = windmill_api::jobs::run_wait_result( &db, Uuid::parse_str(ordered_result_job_id).unwrap(), - "test-workspace".to_string(), + "test-workspace", None, "test-user", ) diff --git a/backend/windmill-api/Cargo.toml b/backend/windmill-api/Cargo.toml index 34cb7adfe9..7b8888b38e 100644 --- a/backend/windmill-api/Cargo.toml +++ b/backend/windmill-api/Cargo.toml @@ -94,6 +94,7 @@ tempfile.workspace = true tokio-util.workspace = true tokio-tar.workspace = true tokio-postgres.workspace = true +postgres-native-tls.workspace = true hmac.workspace = true cookie.workspace = true sha2.workspace = true diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 36d4d30208..e57812e2e2 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.575.0 + version: 1.575.4 title: Windmill API contact: @@ -5956,6 +5956,80 @@ paths: application/json: schema: {} + /w/{workspace}/jobs/run_wait_result/fv/{version}: + post: + summary: run flow by version and wait until completion + operationId: runWaitResultFlowByVersion + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + + requestBody: + description: script args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: job result + content: + application/json: + schema: {} + + get: + summary: run flow by version with GET and wait until completion + operationId: runWaitResultFlowByVersionGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + + responses: + "200": + description: job result + content: + application/json: + schema: {} + /w/{workspace}/jobs/run_and_stream/f/{path}: post: summary: run flow by path and stream updates via SSE @@ -6032,6 +6106,94 @@ paths: schema: type: string + /w/{workspace}/jobs/run_and_stream/fv/{version}: + post: + summary: run flow by version and stream updates via SSE + operationId: runAndStreamFlowByVersion + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + requestBody: + description: flow args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + + get: + summary: run flow by version with GET and stream updates via SSE + operationId: runAndStreamFlowByVersionGet + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - $ref: "#/components/parameters/IncludeHeader" + - $ref: "#/components/parameters/QueueLimit" + - $ref: "#/components/parameters/Payload" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/SkipPreprocessor" + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + - name: poll_delay_ms + description: delay between polling for job updates in milliseconds + in: query + schema: + type: integer + format: int64 + + responses: + "200": + description: server-sent events stream of job updates + content: + text/event-stream: + schema: + type: string + /w/{workspace}/jobs/run_and_stream/p/{path}: post: summary: run script by path and stream updates via SSE @@ -7680,6 +7842,64 @@ paths: type: string format: uuid + /w/{workspace}/jobs/run/fv/{version}: + post: + summary: run flow by version + operationId: runFlowByVersion + tags: + - job + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - name: version + description: flow version ID + in: path + required: true + schema: + type: integer + format: int64 + - name: scheduled_for + description: when to schedule this job (leave empty for immediate run) + in: query + schema: + type: string + format: date-time + - name: scheduled_in_secs + description: schedule the script to execute in the number of seconds starting now + in: query + schema: + type: integer + - $ref: "#/components/parameters/SkipPreprocessor" + - $ref: "#/components/parameters/ParentJob" + - $ref: "#/components/parameters/WorkerTag" + - $ref: "#/components/parameters/NewJobId" + - $ref: "#/components/parameters/IncludeHeader" + - name: invisible_to_owner + description: make the run invisible to the the flow owner (default false) + in: query + schema: + type: boolean + - name: memory_id + description: memory ID for chat-enabled flows + in: query + schema: + type: string + format: uuid + requestBody: + description: flow args + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScriptArgs" + responses: + "201": + description: job created + content: + text/plain: + schema: + type: string + format: uuid + /w/{workspace}/jobs/run/batch_rerun_jobs: post: summary: re-run multiple jobs @@ -8451,6 +8671,7 @@ paths: - $ref: "#/components/parameters/ResultFilter" - $ref: "#/components/parameters/AllowWildcards" - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/JobTriggerKind" - name: is_skipped description: is the job skipped in: query @@ -14424,6 +14645,7 @@ paths: - $ref: "#/components/parameters/AllowWildcards" - $ref: "#/components/parameters/Page" - $ref: "#/components/parameters/PerPage" + - $ref: "#/components/parameters/JobTriggerKind" - name: is_skipped description: is the job skipped in: query @@ -14862,6 +15084,27 @@ components: in: query schema: type: integer + JobTriggerKind: + name: trigger_kind + description: trigger kind (schedule, http, websocket...) + in: query + schema: + type: string + enum: + - webhook + - default_email + - email + - schedule + - http + - websocket + - postgres + - kafka + - nats + - mqtt + - sqs + - gcp + - poll + - cli OrderDesc: name: order_desc description: order by desc order (default true) @@ -18698,6 +18941,8 @@ components: properties: lock_error_logs: type: string + version_id: + type: number ExtraPerms: type: object diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index b669f5e9a9..c7b1bd18bc 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -1241,6 +1241,7 @@ async fn create_app_internal<'a>( false, None, None, + None, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1630,6 +1631,7 @@ async fn update_app_internal<'a>( false, None, None, + None, ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); @@ -1949,6 +1951,7 @@ async fn execute_component( false, end_user_email, None, + None, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/concurrency_groups.rs b/backend/windmill-api/src/concurrency_groups.rs index bfd0b8182f..4a96a16e0b 100644 --- a/backend/windmill-api/src/concurrency_groups.rs +++ b/backend/windmill-api/src/concurrency_groups.rs @@ -222,6 +222,7 @@ async fn get_concurrent_intervals( all_workspaces: _, concurrency_key: Some(_), allow_wildcards: None, + trigger_kind: _, } => true, _ => false, }; diff --git a/backend/windmill-api/src/ee_oss.rs b/backend/windmill-api/src/ee_oss.rs index a69c59c573..30756af8ab 100644 --- a/backend/windmill-api/src/ee_oss.rs +++ b/backend/windmill-api/src/ee_oss.rs @@ -13,7 +13,10 @@ use anyhow::anyhow; #[cfg(all(feature = "enterprise", not(feature = "private")))] use {std::sync::Arc, tokio::sync::RwLock}; #[cfg(not(feature = "private"))] -pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> { +pub async fn validate_license_key( + _license_key: String, + _db: Option<&crate::db::DB>, +) -> anyhow::Result<(String, bool)> { // Implementation is not open source Err(anyhow!("License can't be validated in Windmill CE")) } diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index 9952ca1ddd..dd141d46f6 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -562,6 +562,7 @@ async fn create_flow( false, None, None, + None, ) .await?; @@ -1025,6 +1026,7 @@ async fn update_flow( false, None, None, + None, ) .await?; @@ -1133,6 +1135,7 @@ async fn get_flow_by_path( flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, + flow_version.id AS version_id, flow_version.schema, flow_version.value, flow_version.created_at AS edited_at, @@ -1172,8 +1175,9 @@ async fn get_flow_by_path( flow.timeout, flow.visible_to_runner_only, flow.on_behalf_of_email, + flow_version.id AS version_id, flow_version.schema, - flow_version.value, + flow_version.value, flow_version.created_at AS edited_at, flow_version.created_by AS edited_by, NULL AS starred diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index d695a6091d..dcb2b6cd08 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -34,7 +34,7 @@ use windmill_common::flow_conversations::add_message_to_conversation_tx; use windmill_common::flow_status::{JobResult, RestartedFrom}; use windmill_common::jobs::{ check_tag_available_for_workspace_internal, format_completed_job_result, format_result, - DynamicInput, ENTRYPOINT_OVERRIDE, + DynamicInput, JobTriggerKind, ENTRYPOINT_OVERRIDE, }; use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat}; use windmill_common::utils::{RunnableKind, WarnAfterExt}; @@ -45,7 +45,7 @@ use windmill_common::{email_oss::send_email_html, server::load_smtp_config}; use windmill_common::variables::get_workspace_key; -use crate::triggers::trigger_helpers::ScriptId; +use crate::triggers::trigger_helpers::{FlowId, ScriptId}; use crate::{ add_webhook_allowed_origin, args::{self, RawWebhookArgs}, @@ -95,8 +95,9 @@ use windmill_common::{ }; use windmill_common::{ - get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, - get_script_info_for_hash, utils::empty_as_none, FlowVersionInfo, ScriptHashInfo, BASE_URL, + get_flow_version_info_from_version, get_latest_deployed_hash_for_path, + get_latest_flow_version_info_for_path, get_script_info_for_hash, utils::empty_as_none, + FlowVersionInfo, ScriptHashInfo, BASE_URL, }; use windmill_queue::{ cancel_job, get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs, @@ -124,6 +125,13 @@ pub fn workspaced_service() -> Router { .layer(cors.clone()) .layer(ce_headers.clone()), ) + .route( + "/run/fv/:version", + post(run_flow_by_version) + .head(|| async { "" }) + .layer(cors.clone()) + .layer(ce_headers.clone()), + ) .route( "/run/batch_rerun_jobs", post(batch_rerun_jobs) @@ -176,6 +184,14 @@ pub fn workspaced_service() -> Router { .layer(cors.clone()) .layer(ce_headers.clone()), ) + .route( + "/run_wait_result/fv/:version", + post(run_wait_result_flow_by_version) + .get(run_wait_result_flow_by_version_get) + .head(|| async { "" }) + .layer(cors.clone()) + .layer(ce_headers.clone()), + ) .route( "/run_and_stream/f/*script_path", get(stream_flow_by_path) @@ -184,6 +200,14 @@ pub fn workspaced_service() -> Router { .layer(cors.clone()) .layer(ce_headers.clone()), ) + .route( + "/run_and_stream/fv/:version", + get(stream_flow_by_version) + .post(stream_flow_by_version) + .head(|| async { "" }) + .layer(cors.clone()) + .layer(ce_headers.clone()), + ) .route( "/run_and_stream/p/*script_path", get(stream_script_by_path) @@ -1750,6 +1774,22 @@ impl RunJobQuery { Ok(None) } } + + fn payload_as_args(&self) -> error::Result>> { + let payload_r = self.payload.clone().map(decode_payload).map(|x| { + x.map_err(|e| { + error::Error::internal_err(format!("Impossible to decode query payload: {e:#?}")) + }) + }); + + let payload_as_args = if let Some(payload) = payload_r { + payload? + } else { + HashMap::new() + }; + + Ok(payload_as_args) + } } #[derive(Deserialize, Clone)] @@ -1781,6 +1821,7 @@ pub struct ListQueueQuery { pub is_not_schedule: Option, pub concurrency_key: Option, pub allow_wildcards: Option, + pub trigger_kind: Option, } impl From for ListQueueQuery { @@ -1812,6 +1853,7 @@ impl From for ListQueueQuery { is_not_schedule: lcq.is_not_schedule, concurrency_key: lcq.concurrency_key, allow_wildcards: lcq.allow_wildcards, + trigger_kind: lcq.trigger_kind, } } } @@ -1935,6 +1977,10 @@ pub fn filter_list_queue_query( sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'"); } + if let Some(tk) = &lq.trigger_kind { + sqlb.and_where_eq("trigger_kind", "?".bind(&format!("{}", tk))); + } + sqlb } @@ -2142,7 +2188,7 @@ async fn cancel_selection( let mut tx = user_db.begin(&authed).await?; let tags = get_scope_tags(&authed).map(|v| v.iter().map(|s| s.to_string()).collect_vec()); let jobs_to_cancel = sqlx::query_scalar!( - "SELECT j.id AS \"id!\" FROM v2_job j WHERE j.id = ANY($1) AND j.trigger_kind != 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", + "SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))", &jobs, tags.as_ref().map(|v| v.as_slice()) ) @@ -3851,6 +3897,7 @@ async fn batch_rerun_handle_job( StripPath(job.script_path.clone()), RunJobQuery { skip_preprocessor: Some(true), ..Default::default() }, PushArgsOwned { extra: None, args }, + None, ) .await; if let Ok((uuid, _)) = result { @@ -3867,6 +3914,7 @@ async fn batch_rerun_handle_job( StripPath(job.script_path.clone()), RunJobQuery { skip_preprocessor: Some(true), ..Default::default() }, PushArgsOwned { extra: None, args }, + None, ) .await } else { @@ -3987,28 +4035,22 @@ pub async fn run_flow_by_path( .await?; let (uuid, _) = - run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args).await?; + run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args, None).await?; Ok((StatusCode::CREATED, uuid.to_string())) } -pub async fn run_flow_by_path_inner( - authed: ApiAuthed, - db: DB, +pub async fn run_flow( + authed: &ApiAuthed, + db: &DB, user_db: UserDB, - w_id: String, - flow_path: StripPath, + w_id: &str, + flow_path: &str, + flow_version_info: FlowVersionInfo, run_query: RunJobQuery, args: PushArgsOwned, + trigger_kind: Option, ) -> error::Result<(Uuid, Option)> { - #[cfg(feature = "enterprise")] - check_license_key_valid().await?; - - let flow_path = flow_path.to_path(); - check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; - - let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; - let FlowVersionInfo { version, tag, @@ -4019,8 +4061,7 @@ pub async fn run_flow_by_path_inner( edited_by, early_return, .. - } = get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) - .await?; + } = flow_version_info; let tag = run_query.tag.clone().or(tag); @@ -4078,6 +4119,7 @@ pub async fn run_flow_by_path_inner( false, None, None, + trigger_kind, ) .await?; @@ -4100,9 +4142,146 @@ pub async fn run_flow_by_path_inner( } tx.commit().await?; + Ok((uuid, early_return)) } +pub async fn run_flow_and_wait_result( + authed: &ApiAuthed, + db: &DB, + user_db: UserDB, + w_id: &str, + flow_path: &str, + flow_version_info: FlowVersionInfo, + run_query: RunJobQuery, + args: PushArgsOwned, + trigger_kind: Option, +) -> error::Result { + let (uuid, early_return) = run_flow( + authed, + db, + user_db, + w_id, + flow_path, + flow_version_info, + run_query, + args, + trigger_kind, + ) + .await?; + + run_wait_result(&db, uuid, w_id, early_return, &authed.username).await +} + +pub async fn run_flow_by_path_inner( + authed: ApiAuthed, + db: DB, + user_db: UserDB, + w_id: String, + flow_path: StripPath, + run_query: RunJobQuery, + args: PushArgsOwned, + trigger_kind: Option, +) -> error::Result<(Uuid, Option)> { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = flow_path.to_path(); + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; + + let flow_version_info = + get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) + .await?; + + run_flow( + &authed, + &db, + user_db, + &w_id, + flow_path, + flow_version_info, + run_query, + args, + trigger_kind, + ) + .await +} + +pub async fn run_flow_by_version( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + args: RawWebhookArgs, +) -> error::Result<(StatusCode, String)> { + let args = args + .to_args_from_runnable( + &authed, + &db, + &w_id, + RunnableId::from_flow_version(version), + run_query.skip_preprocessor, + ) + .await?; + + let (uuid, _) = + run_flow_by_version_inner(authed, db, user_db, w_id, version, run_query, args, None) + .await?; + + Ok((StatusCode::CREATED, uuid.to_string())) +} + +pub async fn run_flow_by_version_inner( + authed: ApiAuthed, + db: DB, + user_db: UserDB, + w_id: String, + version: i64, + run_query: RunJobQuery, + args: PushArgsOwned, + trigger_kind: Option, +) -> error::Result<(Uuid, Option)> { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = sqlx::query_scalar!( + r#" + SELECT + path + FROM + flow_version + WHERE + id = $1 AND + workspace_id = $2 + "#, + version, + &w_id + ) + .fetch_one(&db) + .await?; + + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + let flow_version_info = + get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?; + + run_flow( + &authed, + &db, + user_db, + &w_id, + &flow_path, + flow_version_info, + run_query, + args, + trigger_kind, + ) + .await +} + #[cfg(not(feature = "enterprise"))] pub async fn restart_flow( _authed: ApiAuthed, @@ -4195,6 +4374,7 @@ pub async fn restart_flow( false, None, None, + None, ) .await?; tx.commit().await?; @@ -4219,8 +4399,17 @@ pub async fn run_script_by_path( ) .await?; - let (uuid, _) = - run_script_by_path_inner(authed, db, user_db, w_id, script_path, run_query, args).await?; + let (uuid, _) = run_script_by_path_inner( + authed, + db, + user_db, + w_id, + script_path, + run_query, + args, + None, + ) + .await?; Ok((StatusCode::CREATED, uuid.to_string())) } @@ -4233,6 +4422,7 @@ pub async fn run_script_by_path_inner( script_path: StripPath, run_query: RunJobQuery, args: PushArgsOwned, + trigger_kind: Option, ) -> error::Result<(Uuid, Option)> { #[cfg(feature = "enterprise")] check_license_key_valid().await?; @@ -4304,6 +4494,7 @@ pub async fn run_script_by_path_inner( false, None, None, + trigger_kind, ) .await?; tx.commit().await?; @@ -4461,6 +4652,7 @@ pub async fn run_workflow_as_code( false, None, None, + None, ) .await?; @@ -4577,7 +4769,7 @@ pub struct WindmillCompositeResult { pub async fn run_wait_result_internal( db: &DB, uuid: Uuid, - w_id: String, + w_id: &str, node_id_for_empty_return: Option, username: &str, ) -> error::Result<(Box, bool)> { @@ -4593,7 +4785,7 @@ pub async fn run_wait_result_internal( let mut g = Guard { done: false, id: uuid, - w_id: w_id.clone(), + w_id: w_id.to_string(), db: db.clone(), username: username.to_string(), }; @@ -4605,7 +4797,7 @@ pub async fn run_wait_result_internal( if let Some(node_id_for_empty_return) = node_id_for_empty_return.as_ref() { let result_and_success = get_result_and_success_by_id_from_flow( &db, - &w_id, + w_id, &uuid, node_id_for_empty_return, None, @@ -4762,7 +4954,7 @@ pub fn result_to_response(result: Box, success: bool) -> error::Result pub async fn run_wait_result( db: &DB, uuid: Uuid, - w_id: String, + w_id: &str, node_id_for_empty_return: Option, username: &str, ) -> error::Result { @@ -4922,18 +5114,11 @@ pub async fn run_wait_result_job_by_path_get( if method == http::Method::HEAD { return Ok(Json(serde_json::json!("")).into_response()); } - let payload_r = run_query.payload.map(decode_payload).map(|x| { - x.map_err(|e| Error::internal_err(format!("Impossible to decode query payload: {e:#?}"))) - }); - let payload_args = if let Some(payload) = payload_r { - payload? - } else { - HashMap::new() - }; + let payload_as_args = run_query.payload_as_args()?; let mut args = args.process_args(&authed, &db, &w_id, None).await?; - args.body = args::Body::HashMap(payload_args); + args.body = args::Body::HashMap(payload_as_args); let args = args .to_args_from_runnable( @@ -5005,11 +5190,12 @@ pub async fn run_wait_result_job_by_path_get( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; } @@ -5034,20 +5220,10 @@ pub async fn run_wait_result_flow_by_path_get( if method == http::Method::HEAD { return Ok(Json(serde_json::json!("")).into_response()); } - let payload_r = run_query.payload.clone().map(decode_payload).map(|x| { - x.map_err(|e| { - error::Error::internal_err(format!("Impossible to decode query payload: {e:#?}")) - }) - }); - - let payload_args = if let Some(payload) = payload_r { - payload? - } else { - HashMap::new() - }; + let payload_as_args = run_query.payload_as_args()?; let mut args = args.process_args(&authed, &db, &w_id, None).await?; - args.body = args::Body::HashMap(payload_args); + args.body = args::Body::HashMap(payload_as_args); let args = args .to_args_from_runnable( @@ -5159,11 +5335,12 @@ pub async fn run_wait_result_script_by_path_internal( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; } @@ -5281,11 +5458,12 @@ pub async fn run_wait_result_script_by_hash( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; } @@ -5342,6 +5520,28 @@ pub async fn stream_flow_by_path( .await } +pub async fn stream_flow_by_version( + authed: ApiAuthed, + Extension(db): Extension, + Extension(user_db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + method: hyper::http::Method, + args: RawWebhookArgs, +) -> error::Result { + stream_job( + authed, + db, + user_db, + w_id, + RunnableId::from_flow_version(version), + args, + run_query, + method == http::Method::GET, + ) + .await +} + pub async fn stream_script_by_path( authed: ApiAuthed, Extension(db): Extension, @@ -5397,20 +5597,10 @@ pub async fn stream_job( is_get: bool, ) -> error::Result { let args = if is_get { - let payload_r = run_query.payload.clone().map(decode_payload).map(|x| { - x.map_err(|e| { - Error::internal_err(format!("Impossible to decode query payload: {e:#?}")) - }) - }); - - let payload_args = if let Some(payload) = payload_r { - payload? - } else { - HashMap::new() - }; + let payload_as_args = run_query.payload_as_args()?; let mut args = args.process_args(&authed, &db, &w_id, None).await?; - args.body = args::Body::HashMap(payload_args); + args.body = args::Body::HashMap(payload_as_args); let args = args .to_args_from_runnable(&db, &w_id, runnable_id.clone(), run_query.skip_preprocessor) @@ -5439,6 +5629,7 @@ pub async fn stream_job( StripPath(script_path), run_query, args, + None, ) .await? .0 @@ -5456,7 +5647,7 @@ pub async fn stream_job( .await? .0 } - RunnableId::FlowPath(flow_path) => { + RunnableId::FlowId(FlowId::FlowPath(flow_path)) => { run_flow_by_path_inner( authed.clone(), db.clone(), @@ -5465,6 +5656,21 @@ pub async fn stream_job( StripPath(flow_path), run_query, args, + None, + ) + .await? + .0 + } + RunnableId::FlowId(FlowId::FlowVersion(version)) => { + run_flow_by_version_inner( + authed.clone(), + db.clone(), + user_db, + w_id.clone(), + version, + run_query, + args, + None ) .await? .0 @@ -5524,100 +5730,137 @@ pub async fn run_wait_result_flow_by_path_internal( let flow_path = flow_path.to_path(); - let scheduled_for = run_query.get_scheduled_for(&db).await?; - let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() }; - let FlowVersionInfo { - tag, - dedicated_worker, - early_return, - has_preprocessor, - chat_input_enabled, - on_behalf_of_email, - edited_by, - version, - } = get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) - .await?; + let flow_version_info = + get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true) + .await?; - let tag = run_query.tag.clone().or(tag); - check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?; - - let (email, permissioned_as, push_authed, tx) = - if let Some(on_behalf_of_email) = on_behalf_of_email.as_ref() { - ( - on_behalf_of_email, - username_to_permissioned_as(&edited_by), - None, - PushIsolationLevel::IsolatedRoot(db.clone()), - ) - } else { - ( - &authed.email, - username_to_permissioned_as(&authed.username), - Some(authed.clone().into()), - PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()), - ) - }; - - let (uuid, mut tx) = push( + run_flow_and_wait_result( + &authed, &db, - tx, + user_db, &w_id, - JobPayload::Flow { - path: flow_path.to_string(), - dedicated_worker, - version, - apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false) - && has_preprocessor.unwrap_or(false), - }, - PushArgs { args: &args.args, extra: args.extra }, - authed.display_username(), - email, - permissioned_as, - authed.token_prefix.as_deref(), - scheduled_for, - None, - run_query.parent_job, - None, - run_query.root_job, - run_query.job_id, - false, - false, - None, - !run_query.invisible_to_owner.unwrap_or(false), - tag, - None, - None, - None, - push_authed.as_ref(), - false, - None, + flow_path, + flow_version_info, + run_query, + args, None, ) + .await +} + +pub async fn run_wait_result_flow_by_version_get( + method: hyper::http::Method, + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + args: RawWebhookArgs, +) -> error::Result { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = sqlx::query_scalar!( + "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2", + version, + &w_id + ) + .fetch_one(&db) .await?; - // Set conversation_id if provided (for agent memory) - if let Some(memory_id) = run_query.memory_id { - set_flow_memory_id(&mut tx, uuid, memory_id).await?; + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + if method == http::Method::HEAD { + return Ok(Json(serde_json::json!("")).into_response()); } - // Handle conversation messages for chat-enabled flows - if chat_input_enabled.unwrap_or(false) { - handle_chat_conversation_messages( - &mut tx, - &authed, + let payload_as_args = run_query.payload_as_args()?; + + let mut args = args.process_args(&authed, &db, &w_id, None).await?; + args.body = args::Body::HashMap(payload_as_args); + + let args = args + .to_args_from_runnable( + &db, &w_id, - &flow_path.to_string(), - &run_query, - args.args.get("user_message"), + RunnableId::from_flow_version(version), + run_query.skip_preprocessor, ) .await?; - } - tx.commit().await?; + let flow_version_info = + get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?; - run_wait_result(&db, uuid, w_id, early_return, &authed.username).await + run_flow_and_wait_result( + &authed, + &db, + user_db, + &w_id, + &flow_path, + flow_version_info, + run_query, + args, + None, + ) + .await +} + +pub async fn run_wait_result_flow_by_version( + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((w_id, version)): Path<(String, i64)>, + Query(run_query): Query, + args: RawWebhookArgs, +) -> error::Result { + #[cfg(feature = "enterprise")] + check_license_key_valid().await?; + + let flow_path = sqlx::query_scalar!( + r#" + SELECT + path + FROM + flow_version + WHERE + id = $1 AND + workspace_id = $2 + "#, + version, + &w_id + ) + .fetch_one(&db) + .await?; + + check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?; + + let args = args + .to_args_from_runnable( + &authed, + &db, + &w_id, + RunnableId::from_flow_version(version), + run_query.skip_preprocessor, + ) + .await?; + + let flow_version_info = + get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?; + + run_flow_and_wait_result( + &authed, + &db, + user_db, + &w_id, + &flow_path, + flow_version_info, + run_query, + args, + None, + ) + .await } async fn run_preview_script( @@ -5688,6 +5931,7 @@ async fn run_preview_script( false, None, None, + None, ) .await?; tx.commit().await?; @@ -5715,7 +5959,7 @@ async fn run_wait_result_preview_script( let uuid = uuid .parse::() .map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?; - let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; return result; } @@ -5808,6 +6052,7 @@ async fn run_bundle_preview_script( false, None, None, + None, ) .await?; job_id = Some(uuid); @@ -5947,11 +6192,12 @@ async fn run_dependencies_job( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; wait_result } @@ -6016,11 +6262,12 @@ async fn run_flow_dependencies_job( false, None, None, + None, ) .await?; tx.commit().await?; - let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; wait_result } @@ -6369,6 +6616,7 @@ async fn run_preview_flow_job( false, None, None, + None, ) .await?; @@ -6415,7 +6663,7 @@ async fn run_wait_result_preview_flow( let uuid = uuid .parse::() .map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?; - let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await; + let result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await; return result; } @@ -6461,6 +6709,7 @@ async fn run_dynamic_select( StripPath(path), run_query.clone(), push_args.clone(), + None, ) .await?; @@ -6565,6 +6814,7 @@ async fn run_dynamic_select( false, None, None, + None, ) .await?; tx.commit().await?; @@ -6698,6 +6948,7 @@ pub async fn run_job_by_hash_inner( false, None, None, + None, ) .await?; tx.commit().await?; @@ -7666,6 +7917,10 @@ pub fn filter_list_completed_query( sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'"); } + if let Some(tk) = &lq.trigger_kind { + sqlb.and_where_eq("trigger_kind", "?".bind(&format!("{}", tk))); + } + sqlb } @@ -7742,6 +7997,7 @@ pub struct ListCompletedQuery { pub concurrency_key: Option, pub worker: Option, pub allow_wildcards: Option, + pub trigger_kind: Option, } async fn list_completed_jobs( diff --git a/backend/windmill-api/src/public_app_layer.rs b/backend/windmill-api/src/public_app_layer.rs index 7d5e5a6e3b..cce8af1b35 100644 --- a/backend/windmill-api/src/public_app_layer.rs +++ b/backend/windmill-api/src/public_app_layer.rs @@ -71,10 +71,12 @@ fn is_public_route_whitelisted(path: &str) -> bool { "/user/login_callback/*", "/api/workspaces/users", "/api/users/whoami", + "/api/apps_u/*", "/api/oauth/list_connects", "/api/oauth/list_logins", "/public/*", "/a/*", + "/api/oauth/get_connect/*", "/Inter-Variable.woff2", ]; diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index a639c58f6e..82c8864fe3 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -1022,6 +1022,7 @@ async fn create_script_internal<'c>( false, None, None, + None, ) .await?; Ok((hash, new_tx, None)) diff --git a/backend/windmill-api/src/settings.rs b/backend/windmill-api/src/settings.rs index 4851130286..62cb87d391 100644 --- a/backend/windmill-api/src/settings.rs +++ b/backend/windmill-api/src/settings.rs @@ -188,7 +188,7 @@ pub async fn test_license_key( Json(TestKey { license_key }): Json, ) -> error::Result { require_super_admin(&db, &authed.email).await?; - let (_, expired) = validate_license_key(license_key).await?; + let (_, expired) = validate_license_key(license_key, Some(&db)).await?; if expired { Err(error::Error::BadRequest("Expired license key".to_string())) @@ -701,23 +701,49 @@ async fn setup_ducklake_catalog_db_inner( sslmode = ssl_mode ); - let (client, connection) = tokio::time::timeout( - std::time::Duration::from_secs(20), - tokio_postgres::connect(&conn_str, tokio_postgres::NoTls), - ) - .await - .map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))? - .map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?; - let join_handle = tokio::spawn(async move { connection.await }); + let (client, join_handle) = if ssl_mode == "require" { + use native_tls::TlsConnector; + use postgres_native_tls::MakeTlsConnector; + + let mut connector = TlsConnector::builder(); + connector.danger_accept_invalid_certs(true); + connector.danger_accept_invalid_hostnames(true); + + let (client, connection) = tokio::time::timeout( + std::time::Duration::from_secs(20), + tokio_postgres::connect( + &conn_str, + MakeTlsConnector::new(connector.build().map_err(to_anyhow)?), + ), + ) + .await + .map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))? + .map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?; + + let join_handle = tokio::spawn(async move { connection.await }); + (client, join_handle) + } else { + let (client, connection) = tokio::time::timeout( + std::time::Duration::from_secs(20), + tokio_postgres::connect(&conn_str, tokio_postgres::NoTls), + ) + .await + .map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))? + .map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?; + + let join_handle = tokio::spawn(async move { connection.await }); + (client, join_handle) + }; + logs.db_connect = "OK".to_string(); client .batch_execute(&format!( "GRANT CONNECT ON DATABASE \"{dbname}\" TO ducklake_user; - GRANT USAGE ON SCHEMA public TO ducklake_user; - GRANT CREATE ON SCHEMA public TO ducklake_user; - ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;" + GRANT USAGE ON SCHEMA public TO ducklake_user; + GRANT CREATE ON SCHEMA public TO ducklake_user; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;" )) .await .map_err(|e| { diff --git a/backend/windmill-api/src/triggers/http/handler.rs b/backend/windmill-api/src/triggers/http/handler.rs index 5d8898f121..fd051e05be 100644 --- a/backend/windmill-api/src/triggers/http/handler.rs +++ b/backend/windmill-api/src/triggers/http/handler.rs @@ -1050,6 +1050,7 @@ async fn route_job( trigger.error_handler_args.as_ref(), format!("http_trigger/{}", trigger.path), None, + Some(windmill_common::jobs::JobTriggerKind::Http), ) .await .map_err(|e| e.into_response())?; @@ -1108,6 +1109,7 @@ async fn route_job( trigger.error_handler_args.as_ref(), format!("http_trigger/{}", trigger.path), None, + Some(windmill_common::jobs::JobTriggerKind::Http), ) .await .map_err(|e| e.into_response()), @@ -1123,6 +1125,7 @@ async fn route_job( trigger.error_handler_path.as_deref(), trigger.error_handler_args.as_ref(), format!("http_trigger/{}", trigger.path), + Some(windmill_common::jobs::JobTriggerKind::Http), ) .await .map_err(|e| e.into_response()), diff --git a/backend/windmill-api/src/triggers/listener.rs b/backend/windmill-api/src/triggers/listener.rs index 431c4313f9..af9d0e32ce 100644 --- a/backend/windmill-api/src/triggers/listener.rs +++ b/backend/windmill-api/src/triggers/listener.rs @@ -515,6 +515,7 @@ pub trait Listener: TriggerCrud + TriggerJobArgs { error_handler_args, format!("{}_trigger/{}", Self::TRIGGER_KIND, listening_trigger.path), None, + Some(Self::JOB_TRIGGER_KIND), ) .await?; diff --git a/backend/windmill-api/src/triggers/trigger_helpers.rs b/backend/windmill-api/src/triggers/trigger_helpers.rs index 7deaf6dc1d..e592c43bb3 100644 --- a/backend/windmill-api/src/triggers/trigger_helpers.rs +++ b/backend/windmill-api/src/triggers/trigger_helpers.rs @@ -12,7 +12,10 @@ use windmill_common::{ error::Result, flows::{FlowModuleValue, Retry}, get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path, - jobs::{get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload}, + jobs::{ + get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload, + JobTriggerKind, + }, scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang}, triggers::{ HubOrWorkspaceId, RunnableFormat, RunnableFormatVersion, TriggerKind, @@ -21,7 +24,6 @@ use windmill_common::{ users::username_to_permissioned_as, utils::StripPath, worker::to_raw_value, - FlowVersionInfo, }; use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel}; @@ -56,7 +58,7 @@ struct PartialSchema { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum RunnableId { - FlowPath(String), + FlowId(FlowId), ScriptId(ScriptId), HubScript(String), } @@ -75,7 +77,33 @@ impl RunnableId { } pub fn from_flow_path(path: &str) -> Self { - Self::FlowPath(path.to_string()) + Self::FlowId(FlowId::FlowPath(path.to_string())) + } + + pub fn from_flow_version(version: i64) -> Self { + Self::FlowId(FlowId::FlowVersion(version)) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FlowId { + FlowPath(String), + FlowVersion(i64), +} + +impl FlowId { + async fn get_flow_version_id(self, workspace_id: &str, db: &DB) -> Result { + let version_id = match self { + FlowId::FlowPath(path) => { + let info = + get_latest_flow_version_info_for_path(None, db, workspace_id, &path, true) + .await?; + info.version + } + FlowId::FlowVersion(version) => version, + }; + + Ok(version_id) } } @@ -255,9 +283,8 @@ pub async fn get_runnable_format( }, ) } - RunnableId::FlowPath(path) => { - let FlowVersionInfo { version, .. } = - get_latest_flow_version_info_for_path(None, &db, workspace_id, &path, true).await?; + RunnableId::FlowId(flow_id) => { + let version = flow_id.get_flow_version_id(workspace_id, db).await?; let key = ( HubOrWorkspaceId::WorkspaceId(workspace_id.to_string()), @@ -268,7 +295,7 @@ pub async fn get_runnable_format( let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key); if let Some(runnable_format) = runnable_format { - tracing::debug!("Using cached runnable format for flow {path}"); + tracing::debug!("Using cached runnable format for flow version {version}"); return Ok(runnable_format); } @@ -279,11 +306,9 @@ pub async fn get_runnable_format( schema as \"schema: _\" FROM flow_version WHERE - path = $1 - AND workspace_id = $2 - ORDER BY created_at DESC - LIMIT 1", - path, + id = $1 + AND workspace_id = $2", + version, workspace_id, ) .fetch_one(db) @@ -494,6 +519,7 @@ pub async fn trigger_runnable_inner( error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result<(Uuid, Option, Option)> { let error_handler_args = error_handler_args.map(|args| { let args = args @@ -516,6 +542,7 @@ pub async fn trigger_runnable_inner( path, run_query, args, + trigger_kind, ) .await?; (uuid, None, early_return) @@ -532,6 +559,7 @@ pub async fn trigger_runnable_inner( error_handler_args.as_ref(), trigger_path, job_id, + trigger_kind, ) .await?; (uuid, delete_after_use, None) @@ -554,6 +582,7 @@ pub async fn trigger_runnable( error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result { let (uuid, _, _) = trigger_runnable_inner( db, @@ -568,6 +597,7 @@ pub async fn trigger_runnable( error_handler_args, trigger_path, job_id, + trigger_kind, ) .await?; Ok((StatusCode::CREATED, uuid.to_string()).into_response()) @@ -586,6 +616,7 @@ pub async fn trigger_runnable_and_wait_for_result( error_handler_path: Option<&str>, error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, + trigger_kind: Option, ) -> Result { let username = authed.username.clone(); let (uuid, delete_after_use, early_return) = trigger_runnable_inner( @@ -601,11 +632,11 @@ pub async fn trigger_runnable_and_wait_for_result( error_handler_args, trigger_path, None, + trigger_kind, ) .await?; let (result, success) = - run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username) - .await?; + run_wait_result_internal(db, uuid, &workspace_id, early_return, &username).await?; if delete_after_use.unwrap_or(false) { delete_job_metadata_after_use(&db, uuid).await?; @@ -627,6 +658,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result( error_handler_path: Option<&str>, error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, + trigger_kind: Option, ) -> Result<(Box, bool)> { let username = authed.username.clone(); let (uuid, delete_after_use, early_return) = trigger_runnable_inner( @@ -642,11 +674,12 @@ pub async fn trigger_runnable_and_wait_for_raw_result( error_handler_args, trigger_path, None, + trigger_kind, ) .await?; let (result, success) = - run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username) + run_wait_result_internal(db, uuid, &workspace_id, early_return, &username) .await .with_context(|| { format!( @@ -675,6 +708,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx( error_handler_path: Option<&str>, error_handler_args: Option<&sqlx::types::Json>>, trigger_path: String, + trigger_kind: Option, ) -> Result> { let (result, success) = trigger_runnable_and_wait_for_raw_result( db, @@ -688,6 +722,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx( error_handler_path, error_handler_args, trigger_path, + trigger_kind, ) .await?; @@ -714,6 +749,7 @@ async fn trigger_script_internal( error_handler_args: Option<&sqlx::types::Json>>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result<(Uuid, Option)> { if retry.is_none() && error_handler_path.is_none() { let run_query = RunJobQuery { job_id, ..Default::default() }; @@ -726,6 +762,7 @@ async fn trigger_script_internal( path, run_query, args, + trigger_kind, ) .await } else { @@ -741,6 +778,7 @@ async fn trigger_script_internal( error_handler_args, trigger_path, job_id, + trigger_kind, ) .await } @@ -758,6 +796,7 @@ async fn trigger_script_with_retry_and_error_handler( error_handler_args: Option<&sqlx::types::Json>>>, trigger_path: String, job_id: Option, + trigger_kind: Option, ) -> Result<(Uuid, Option)> { #[cfg(feature = "enterprise")] check_license_key_valid().await?; @@ -870,6 +909,7 @@ async fn trigger_script_with_retry_and_error_handler( false, None, None, + trigger_kind, ) .await?; tx.commit().await?; diff --git a/backend/windmill-api/src/triggers/websocket/listener.rs b/backend/windmill-api/src/triggers/websocket/listener.rs index bfecaf479d..a89532566a 100644 --- a/backend/windmill-api/src/triggers/websocket/listener.rs +++ b/backend/windmill-api/src/triggers/websocket/listener.rs @@ -94,6 +94,7 @@ impl ListeningTrigger { None, None, "".to_string(), // doesn't matter as no retry/error handler + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) .await .map(|r| r.get().to_owned())?; @@ -392,6 +393,7 @@ impl Listener for WebsocketTrigger { error_handler_path.as_deref(), error_handler_args.as_ref(), format!("websocket_trigger/{}", trigger_path), + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) => { if let Ok((result, success)) = result { if !success && !can_return_error_result { @@ -428,6 +430,7 @@ impl Listener for WebsocketTrigger { error_handler_args, format!("websocket_trigger/{}", listening_trigger.path), None, + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) .await?; } diff --git a/backend/windmill-api/src/triggers/websocket/mod.rs b/backend/windmill-api/src/triggers/websocket/mod.rs index 7a4d98bcc1..e19c958423 100644 --- a/backend/windmill-api/src/triggers/websocket/mod.rs +++ b/backend/windmill-api/src/triggers/websocket/mod.rs @@ -2,7 +2,9 @@ use std::collections::HashMap; use crate::{ db::ApiAuthed, - triggers::trigger_helpers::{trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs}, + triggers::trigger_helpers::{ + trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs, + }, }; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -64,9 +66,8 @@ pub fn value_to_args_hashmap( args: Option<&Box>, ) -> Result>> { let args = if let Some(args) = args { - let args_map: Option> = - serde_json::from_str(args.get()) - .map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?; + let args_map: Option> = serde_json::from_str(args.get()) + .map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?; args_map .unwrap_or_else(HashMap::new) @@ -112,6 +113,7 @@ pub async fn get_url_from_runnable_value( None, None, "".to_string(), // doesn't matter as no retry/error handler + Some(windmill_common::jobs::JobTriggerKind::Websocket), ) .await?; diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index 929736eff5..5b81d35f75 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -66,6 +66,7 @@ pub struct FlowWithStarred { pub starred: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lock_error_logs: Option, + pub version_id: i64, } fn is_none_or_false(b: &Option) -> bool { diff --git a/backend/windmill-common/src/lib.rs b/backend/windmill-common/src/lib.rs index b0a3f5e4ea..b91dcfaf95 100644 --- a/backend/windmill-common/src/lib.rs +++ b/backend/windmill-common/src/lib.rs @@ -769,7 +769,7 @@ where } } -pub fn get_latest_flow_version_info_for_path_from_version< +pub fn get_flow_version_info_from_version< 'a, 'e, A: sqlx::Acquire<'e, Database = Postgres> + Send + 'a, @@ -782,7 +782,6 @@ pub fn get_latest_flow_version_info_for_path_from_version< async move { // as instructed in the docstring of sqlx::Acquire let key = (w_id.to_string(), version); - match FLOW_INFO_CACHE.get(&key) { Some(info) => { tracing::debug!("Using cached flow version info for {version} ({path})"); @@ -791,21 +790,37 @@ pub fn get_latest_flow_version_info_for_path_from_version< _ => { tracing::debug!("Fetching flow version info for {version} ({path})"); let mut conn = db.acquire().await?; - let info = sqlx::query_as!( - FlowVersionInfo, - "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version - FROM flow - INNER JOIN flow_version - ON flow_version.id = $3 - WHERE flow.path = $1 and flow.workspace_id = $2", - path, - w_id, - version - ) - .fetch_optional(&mut *conn) - .await?; + let flow_info = + sqlx::query_as!( + FlowVersionInfo, + r#" + SELECT + flow_version.id AS version, + flow_version.value->>'early_return' as early_return, + flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, + (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, + flow.tag, + flow.dedicated_worker, + flow.on_behalf_of_email, + flow.edited_by + FROM + flow_version + INNER JOIN flow + ON flow.path = flow_version.path AND + flow.workspace_id = flow_version.workspace_id + WHERE + flow_version.workspace_id = $1 AND + flow_version.path = $2 AND + flow_version.id = $3 + "#, + w_id, + path, + version, + ) + .fetch_optional(&mut *conn) + .await?; - let info = utils::not_found_if_none(info, "flow", path)?; + let info = utils::not_found_if_none(flow_info, "flow", path)?; FLOW_INFO_CACHE.insert(key, info.clone()); @@ -825,7 +840,7 @@ pub async fn get_latest_flow_version_info_for_path<'e>( // as instructed in the docstring of sqlx::Acquire let version = get_latest_flow_version_id_for_path(db_authed, &db.clone(), w_id, path, use_cache).await?; - get_latest_flow_version_info_for_path_from_version(db, version, w_id, path).await + get_flow_version_info_from_version(db, version, w_id, path).await } async fn get_latest_flow_version_for_path<'e, E: sqlx::PgExecutor<'e>>( diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 9f02f4be26..7783751944 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -470,6 +470,7 @@ pub async fn push_init_job<'c>( false, None, None, + None, ) .await?; inner_tx.commit().await?; @@ -528,6 +529,7 @@ pub async fn push_periodic_bash_job<'c>( false, None, None, + None, ) .await?; inner_tx.commit().await?; @@ -1373,6 +1375,7 @@ async fn restart_job_if_perpetual_inner( false, None, None, + None, ) .await?; tx.commit().await?; @@ -1923,6 +1926,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>( false, None, None, + None, ) .await?; tx.commit().await?; @@ -3882,6 +3886,7 @@ pub async fn push<'c, 'd>( // If we know there is already a debounce job, we can use this for debouncing. // NOTE: Only works with dependency jobs triggered by relative imports debounce_job_id_o: Option, + trigger_kind: Option, ) -> Result<(Uuid, Transaction<'c, Postgres>), Error> { #[cfg(feature = "cloud")] if *CLOUD_HOSTED { @@ -5277,11 +5282,13 @@ pub async fn push<'c, 'd>( // tracing::error!("Could not insert job_perms for job {job_id}: {err:#}"); // } - let trigger_kind = if schedule_path.is_some() { - Some(JobTriggerKind::Schedule) - } else { - None - }; + let trigger_kind = trigger_kind.or_else(|| { + if schedule_path.is_some() { + Some(JobTriggerKind::Schedule) + } else { + None + } + }); let root_job = if root_job.is_some() && (root_job == flow_innermost_root_job.or(parent_job).or(Some(job_id))) diff --git a/backend/windmill-queue/src/schedule.rs b/backend/windmill-queue/src/schedule.rs index aed22e3f28..52a397ce33 100644 --- a/backend/windmill-queue/src/schedule.rs +++ b/backend/windmill-queue/src/schedule.rs @@ -17,8 +17,8 @@ use std::str::FromStr; use windmill_common::db::Authed; use windmill_common::ee_oss::LICENSE_KEY_VALID; use windmill_common::flows::Retry; +use windmill_common::get_flow_version_info_from_version; use windmill_common::get_latest_flow_version_id_for_path; -use windmill_common::get_latest_flow_version_info_for_path_from_version; use windmill_common::jobs::check_tag_available_for_workspace_internal; use windmill_common::jobs::JobPayload; use windmill_common::schedule::schedule_to_user; @@ -63,7 +63,7 @@ async fn get_schedule_metadata<'c>( .await?; let FlowVersionInfo { tag, on_behalf_of_email, edited_by, .. } = - get_latest_flow_version_info_for_path_from_version( + get_flow_version_info_from_version( &mut **tx, version, &schedule.workspace_id, @@ -291,16 +291,13 @@ pub async fn push_scheduled_job<'c>( let FlowVersionInfo { version, tag, dedicated_worker, on_behalf_of_email, edited_by, .. - } = get_latest_flow_version_info_for_path_from_version( + } = get_flow_version_info_from_version( &mut *tx, version, &schedule.workspace_id, &schedule.script_path, ) - .warn_after_seconds_with_sql( - 1, - "get_latest_flow_version_info_for_path_from_version".to_string(), - ) + .warn_after_seconds_with_sql(1, "get_flow_version_info_from_version".to_string()) .await?; ( @@ -503,6 +500,7 @@ pub async fn push_scheduled_job<'c>( false, None, None, + Some(windmill_common::jobs::JobTriggerKind::Schedule), ) .warn_after_seconds_with_sql(1, "push in push_scheduled_job".to_string()) .await?; diff --git a/backend/windmill-worker/src/ai/tools.rs b/backend/windmill-worker/src/ai/tools.rs index 74f46c2bf4..478eca5e54 100644 --- a/backend/windmill-worker/src/ai/tools.rs +++ b/backend/windmill-worker/src/ai/tools.rs @@ -471,6 +471,7 @@ async fn execute_windmill_tool( true, None, None, + None, ) .await?; diff --git a/backend/windmill-worker/src/js_eval.rs b/backend/windmill-worker/src/js_eval.rs index 5827c8ba54..5feeab8003 100644 --- a/backend/windmill-worker/src/js_eval.rs +++ b/backend/windmill-worker/src/js_eval.rs @@ -171,6 +171,76 @@ const DOT_PATTERN: &'static str = "."; const START_BRACKET_PATTERN: &'static str = "[\""; const END_BRACKET_PATTERN: &'static str = "\"]"; +/// Determines if we should prepend "return" to the expression +#[cfg(feature = "deno_core")] +fn should_add_return(expr: &str) -> bool { + // Trim whitespace + let trimmed = expr.trim(); + + // If it's empty, add return + if trimmed.is_empty() { + return true; + } + + // Check if it already starts with 'return' keyword (as a statement) + // Use word boundary to avoid matching "return" in variable names + if trimmed.starts_with("return ") || trimmed.starts_with("return;") || trimmed == "return" { + return false; + } + + // Check for common statement patterns that shouldn't have return prepended + let statement_prefixes = [ + "const ", "let ", "var ", "if ", "if(", "for ", "for(", + "while ", "while(", "switch ", "switch(", "try ", "try{", + "throw ", "function ", "class ", "async ", "await " + ]; + + for prefix in &statement_prefixes { + if trimmed.starts_with(prefix) { + return false; + } + } + + // Check for multiple statements (contains semicolon not in a string) + // This is still not perfect but better than current logic + if contains_semicolon_outside_strings(trimmed) { + return false; + } + + // Default: assume it's an expression that needs return + true +} + +/// Checks if the expression contains a semicolon outside of strings +#[cfg(feature = "deno_core")] +fn contains_semicolon_outside_strings(expr: &str) -> bool { + let mut in_single_quote = false; + let mut in_double_quote = false; + let mut in_template = false; + let mut prev_char = '\0'; + + for ch in expr.chars() { + match ch { + '\'' if prev_char != '\\' && !in_double_quote && !in_template => { + in_single_quote = !in_single_quote; + } + '"' if prev_char != '\\' && !in_single_quote && !in_template => { + in_double_quote = !in_double_quote; + } + '`' if prev_char != '\\' && !in_single_quote && !in_double_quote => { + in_template = !in_template; + } + ';' if !in_single_quote && !in_double_quote && !in_template => { + return true; + } + _ => {} + } + prev_char = ch; + } + + false +} + fn try_exact_property_access( expr: &str, flow_input: Option<&mappable_rc::Marc>>>, @@ -574,10 +644,10 @@ async function resource(path) {{ (String::new(), String::new()) }; - let f = if expr.contains("return ") { - expr.to_string() - } else { + let f = if should_add_return(expr) { format!("return {expr}") + } else { + expr.to_string() }; let ctx_str = ctx @@ -1492,4 +1562,74 @@ multiline template`"; // assert_eq!(res.0.get(), "\"\""); // Ok(()) // } + + #[test] + fn test_should_add_return() { + // Simple expressions should get return added + assert_eq!(should_add_return("5"), true); + assert_eq!(should_add_return("x + y"), true); + assert_eq!(should_add_return("foo()"), true); + assert_eq!(should_add_return("obj.property"), true); + + // Object literals should get return added + assert_eq!(should_add_return("{ foo: 'bar' }"), true); + assert_eq!(should_add_return("{ a: 1, b: 2 }"), true); + assert_eq!(should_add_return("{}"), true); + + // Already has return + assert_eq!(should_add_return("return 5"), false); + assert_eq!(should_add_return("return x + y"), false); + assert_eq!(should_add_return("return;"), false); + assert_eq!(should_add_return("return"), false); + + // Should NOT add return for statements + assert_eq!(should_add_return("const x = 5"), false); + assert_eq!(should_add_return("let y = 10"), false); + assert_eq!(should_add_return("var z = 15"), false); + assert_eq!(should_add_return("if (x > 5) { return x; }"), false); + assert_eq!(should_add_return("for (let i = 0; i < 10; i++) {}"), false); + assert_eq!(should_add_return("while (true) {}"), false); + assert_eq!(should_add_return("function foo() {}"), false); + assert_eq!(should_add_return("throw new Error('test')"), false); + + // Multiple statements with semicolons (including block statements) + assert_eq!(should_add_return("let x = 5; x + 1"), false); + assert_eq!(should_add_return("{ const x = 5; return x; }"), false); + + // Edge case: "return" in a string should still get return prepended + assert_eq!(should_add_return("\"return this string\""), true); + assert_eq!(should_add_return("'return in single quotes'"), true); + assert_eq!(should_add_return("`return in template literal`"), true); + + // Semicolons in strings should not trigger multi-statement detection + assert_eq!(should_add_return("\"hello; world\""), true); + assert_eq!(should_add_return("'test; string'"), true); + assert_eq!(should_add_return("`template; literal`"), true); + } + + #[test] + fn test_contains_semicolon_outside_strings() { + // Semicolons outside strings + assert_eq!(contains_semicolon_outside_strings("let x = 5; x + 1"), true); + assert_eq!(contains_semicolon_outside_strings("x; y"), true); + + // Semicolons inside strings (should NOT be detected) + assert_eq!(contains_semicolon_outside_strings("\"hello; world\""), false); + assert_eq!(contains_semicolon_outside_strings("'test; string'"), false); + assert_eq!(contains_semicolon_outside_strings("`template; literal`"), false); + + // Mixed cases + assert_eq!( + contains_semicolon_outside_strings("let x = 'hello; world'; x"), + true + ); + assert_eq!( + contains_semicolon_outside_strings("console.log(\"test; string\")"), + false + ); + + // No semicolons + assert_eq!(contains_semicolon_outside_strings("x + y"), false); + assert_eq!(contains_semicolon_outside_strings("foo()"), false); + } } diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index f550bcd09d..f0bc4cfc1e 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -3303,6 +3303,7 @@ async fn push_next_flow_job( false, None, None, + None, ) .warn_after_seconds(2) .await?; diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 44b68948a7..0b840884f5 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -670,6 +670,7 @@ pub async fn trigger_dependents_to_recompute_dependencies( false, None, debounce_job_id_o, + None, ) .await?; diff --git a/benchmarks/lib.ts b/benchmarks/lib.ts index 4038cd90b3..d5d3bd172f 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.575.0"; +export const VERSION = "v1.575.4"; export async function login(email: string, password: string): Promise { return await windmill.UserService.login({ diff --git a/cli/src/main.ts b/cli/src/main.ts index 42d17a4231..6dba2bce7a 100644 --- a/cli/src/main.ts +++ b/cli/src/main.ts @@ -68,7 +68,7 @@ export { // } // }); -export const VERSION = "1.575.0"; +export const VERSION = "1.575.4"; export const WM_FORK_PREFIX = "wm-fork"; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d651c99a6f..2c35d77034 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill-components", - "version": "1.575.0", + "version": "1.575.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windmill-components", - "version": "1.575.0", + "version": "1.575.4", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -24,7 +24,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@scalar/openapi-parser": "^0.15.0", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", - "@tutorlatin/svelte-tiny-virtual-list": "^3.0.2", + "@tutorlatin/svelte-tiny-virtual-list": "^3.0.16", "@windmill-labs/svelte-dnd-action": "^0.9.44", "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^1.0.0", @@ -3020,9 +3020,9 @@ } }, "node_modules/@tutorlatin/svelte-tiny-virtual-list": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.15.tgz", - "integrity": "sha512-ew61aZNXGf0b5X+UjbOAhiNwzI21vijhB/mtBs8bpNOVYQ50TG6Qx00t+fR5C72eGnmdzguewZ2WP6QPNOTQJg==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.16.tgz", + "integrity": "sha512-JQSmhRDAFZbq2rTlzn+kFXJayi5VPLxeGjD01ZVyV2ti7PlQE/ov6rQFR1c8s7Y3B1OiTcv3oEWGi3ib69V8eQ==", "license": "MIT", "engines": { "node": ">=20.17.0" @@ -13936,29 +13936,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml-utils": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index dfb0fda59a..7bd2758467 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill-components", - "version": "1.575.0", + "version": "1.575.4", "scripts": { "dev": "vite dev", "build": "vite build", @@ -89,7 +89,7 @@ "@redocly/json-to-json-schema": "^0.0.1", "@scalar/openapi-parser": "^0.15.0", "@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1", - "@tutorlatin/svelte-tiny-virtual-list": "^3.0.2", + "@tutorlatin/svelte-tiny-virtual-list": "^3.0.16", "@windmill-labs/svelte-dnd-action": "^0.9.44", "@xterm/addon-fit": "^0.10.0", "@xyflow/svelte": "^1.0.0", diff --git a/frontend/src/lib/cancelable-promise-utils.ts b/frontend/src/lib/cancelable-promise-utils.ts new file mode 100644 index 0000000000..17e8be5a93 --- /dev/null +++ b/frontend/src/lib/cancelable-promise-utils.ts @@ -0,0 +1,84 @@ +import { CancelablePromise } from './gen' + +export namespace CancelablePromiseUtils { + export function then( + promise: CancelablePromise, + f: (value: T) => CancelablePromise + ): CancelablePromise { + let promiseToBeCanceled: CancelablePromise = promise + let p = new CancelablePromise((resolve, reject) => { + promise + .then((value1) => { + let promise2 = f(value1) + promiseToBeCanceled = promise2 + promise2.then((value2) => resolve(value2)).catch((err) => reject(err)) + }) + .catch((err) => reject(err)) + }) + p.cancel = () => promiseToBeCanceled.cancel() + return p + } + + export function pure(value: T): CancelablePromise { + return new CancelablePromise((resolve) => resolve(value)) + } + + export function err(error: any): CancelablePromise { + return new CancelablePromise((_, reject) => reject(error)) + } + + export function map( + promise: CancelablePromise, + f: (value: T) => U + ): CancelablePromise { + return then(promise, (value) => pure(f(value))) + } + + export function pipe( + promise: CancelablePromise, + f: (value: T) => void + ): CancelablePromise { + promise.then((value) => { + f(value) + }) + return promise + } + + export function catchErr( + promise: CancelablePromise, + f: (error: any) => CancelablePromise + ): CancelablePromise { + let promiseToBeCanceled: CancelablePromise = promise + let p = new CancelablePromise((resolve, reject) => { + promise + .then((value) => resolve(value)) + .catch((err) => { + let promise2 = f(err) + promiseToBeCanceled = promise2 + return promise2.then((value2) => resolve(value2)).catch((err2) => reject(err2)) + }) + .catch((err) => reject(err)) + }) + p.cancel = () => promiseToBeCanceled.cancel() + return p + } + + export function finallyDo(promise: CancelablePromise, f: () => void): CancelablePromise { + promise = map(promise, (value) => (f(), value)) + promise = catchErr(promise, (e) => (f(), err(e))) + return promise + } + + // Calls onTimeout if the promise does not settle within timeoutMs milliseconds + export function onTimeout( + promise: CancelablePromise, + timeoutMs: number, + onTimeout: () => void + ): CancelablePromise { + let timeoutId: number | undefined = setTimeout(onTimeout, timeoutMs) + promise = finallyDo(promise, () => { + if (timeoutId !== undefined) clearTimeout(timeoutId) + }) + return promise + } +} diff --git a/frontend/src/lib/components/AppConnectInner.svelte b/frontend/src/lib/components/AppConnectInner.svelte index 817a6f4f16..6e4854e550 100644 --- a/frontend/src/lib/components/AppConnectInner.svelte +++ b/frontend/src/lib/components/AppConnectInner.svelte @@ -33,6 +33,7 @@ import TextInput from './text_input/TextInput.svelte' import { usePromise } from '$lib/svelte5Utils.svelte' import { pollJobResult } from './jobs/utils' + import { sameTopDomainOrigin } from '$lib/cookies' interface Props { step?: number @@ -239,7 +240,7 @@ function popupListener(event) { console.log('Received oauth popup message', event) let data = event.data - if (event.origin == null || event.origin !== window.location.origin) { + if (!sameTopDomainOrigin(event.origin, window.location.origin)) { console.log( 'Received oauth popup message from different origin', event.origin, @@ -397,6 +398,7 @@ } window.addEventListener('message', popupListener) window.addEventListener('storage', handleStorageEvent) + console.log('opening popup', url.toString()) window.open(url.toString(), '_blank', 'popup=true') step += 1 } diff --git a/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte b/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte index 9e3fe4b55a..81dd26c04b 100644 --- a/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte +++ b/frontend/src/lib/components/AppConnectLightweightResourcePicker.svelte @@ -42,7 +42,7 @@ {/if} - +
+ +
{/if} diff --git a/frontend/src/lib/components/RunsPage.svelte b/frontend/src/lib/components/RunsPage.svelte new file mode 100644 index 0000000000..86bcac405d --- /dev/null +++ b/frontend/src/lib/components/RunsPage.svelte @@ -0,0 +1,1389 @@ + + + + + + + { + const func = askingForConfirmation?.onConfirm + await func?.(forceCancelInPopup) + askingForConfirmation = undefined + }} + type={askingForConfirmation?.type} + loading={askingForConfirmation?.loading} + on:canceled={() => { + askingForConfirmation = undefined + }} +> + {#if askingForConfirmation?.preContent} +
{askingForConfirmation.preContent}
+ + {#if forceCancelInPopup} +
+

+ Force cancel is enabled. This is dangerous, only do this if you have no alternatives. + Instead of being gracefully cancelled, all jobs will be immediately sent to the completed + job table regardless of them being processed or not or part of running flows. You may end + up in an inconsistent state. +

+
+ {/if} + {/if} +
+ + + + {#if selectedIds.length === 1} + {#if selectedIds[0] === '-'} +
There is no information available for this job
+ {:else} + + {/if} + {/if} +
+
+ + { + reset() + loadFromQuery() + }} +/> + +{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.runs} + +{:else} +
+ +
+
+
+

+ Runs +

+ + + All past and schedule executions of scripts and flows, including previews. You only see + your own runs or runs of groups you belong to unless you are an admin. + +
+ + + { + jobsFilter('waiting') + }} + onJobsSuspended={() => { + jobsFilter('suspended') + }} + small={innerWidth < smallScreenWidth} + /> +
+ +
+ +
+ + {#if minTs || maxTs} + + {/if} + { + minTs = new Date(detail).toISOString() + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + on:clear={async () => { + minTs = undefined + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + /> + + + + {#if maxTs || minTs} + + {/if} + { + maxTs = new Date(detail).toISOString() + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + on:clear={async () => { + maxTs = undefined + calendarChangeTimeout && clearTimeout(calendarChangeTimeout) + calendarChangeTimeout = setTimeout(() => { + jobsLoader?.loadJobs(minTs, maxTs, true) + }, 1000) + }} + /> + + + {#if minTs || maxTs} + + + + {/if} +
+ + +
+ { + if (e.detail == 'running' && maxTs != undefined) { + maxTs = undefined + } + }} + {usernames} + {folders} + {paths} + mobile={innerWidth < verySmallScreenWidth} + small={innerWidth < smallScreenWidth} + calendarSmall={!minTs && !maxTs} + /> +
+
+
+ + +
+
+
+ { + graph = detail + graphIsRunsChart = graph === 'RunChart' + }} + > + {#snippet children({ item })} + + + {/snippet} + + + {#if !graphIsRunsChart} + setLookback(0), + id: '0' + }, + { + displayName: '1 day', + action: () => setLookback(1), + id: '1' + }, + { + displayName: '3 days', + action: () => setLookback(3), + id: '3' + }, + { + displayName: '7 days', + action: () => setLookback(7), + id: '7' + } + ]} + selected={lookback.toString()} + selectedDisplayName={`${lookback} days lookback`} + > + {#snippet extraLabel()} + + {#snippet text()} + How far behind the min datetime to start considering jobs for the concurrency + graph. Change this value to include jobs started before the set time window for + the computation of the graph + {/snippet} + + {/snippet} + + {/if} +
+
+ {#if graph === 'RunChart'} + { + minTs = zoom.min.toISOString() + maxTs = zoom.max.toISOString() + manualDatePicker?.resetChoice() + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + onPointClicked={(ids) => { + runsTable?.scrollToRun(ids) + }} + /> + {:else if graph === 'ConcurrencyChart'} + { + minTs = zoom.min.toISOString() + maxTs = zoom.max.toISOString() + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + /> + {/if} +
+ +
+ + +
+ +
+
+ {#if selectionMode && selectableJobCount} +
+
+ +
+ +
+ {/if} + + +
+ +
+ {#if !jobTriggerKind} +
+ { + localStorage.setItem( + 'show_schedules_in_run', + showSchedules ? 'true' : 'false' + ) + }} + options={tableTopBarWidth < 800 || selectionMode + ? {} + : { right: 'Schedules' }} + /> + + + +
+ {/if} + +
+ { + localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') + }} + id="planned-later" + options={tableTopBarWidth < 800 || selectionMode + ? {} + : { right: 'Planned later' }} + /> + + + +
+
+ { + lastFetchWentToEnd = false + jobsLoader?.loadJobs(minTs, maxTs, true) + }} + bind:minTs + bind:maxTs + bind:selectedManualDate + {loading} + bind:this={manualDatePicker} + numberOfLastJobsToFetch={perPage} + /> + { + localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') + }} + options={{ right: 'Auto-refresh' }} + textClass="whitespace-nowrap" + /> +
+
+
+ + +
+ {#if jobs} + + {:else} +
+ {#each new Array(8) as _} + + {/each} +
+ {/if} +
+
+ Per page: + - {#if usernames} - {#if $userStore?.is_admin || $userStore?.is_super_admin} - - {/if} - {#each usernames as e} - {#if e == username || $userStore?.is_admin || $userStore?.is_super_admin} - - {:else} - - {/if} - {/each} - {/if} - + (resources.value?.push(r), (resource = r))} createText="Press enter to use this value" bind:value={resource} @@ -423,6 +401,7 @@ Operation - - {#each ['Create', 'Update', 'Delete', 'Execute'] as e} - - {/each} - + ({ + label: triggerDisplayNamesMap[value], + value + }))} + bind:value={jobTriggerKind} + clearable + />
diff --git a/frontend/src/lib/components/runs/RunsTable.svelte b/frontend/src/lib/components/runs/RunsTable.svelte index 341d40d834..3eb22b2af2 100644 --- a/frontend/src/lib/components/runs/RunsTable.svelte +++ b/frontend/src/lib/components/runs/RunsTable.svelte @@ -22,6 +22,7 @@ activeLabel?: string | null // const loadMoreQuantity: number = 100 lastFetchWentToEnd?: boolean + perPage?: number } let { @@ -33,7 +34,8 @@ selectedIds = $bindable([]), selectedWorkspace = $bindable(undefined), activeLabel = null, - lastFetchWentToEnd = $bindable(false) + lastFetchWentToEnd = $bindable(false), + perPage = 1000 }: Props = $props() function getTime(job: Job): string | undefined { @@ -116,7 +118,7 @@ let tableHeight: number = $state(0) let headerHeight: number = $state(0) let containerWidth: number = $state(0) - // const MAX_ITEMS = 1000 + // const MAX_ITEMS = perPage /* function infiniteHandler({ detail: { loaded, error, complete } }) { @@ -139,7 +141,7 @@ return '' } const jc = jobCount - const isTruncated = jc >= 1000 && !lastFetchWentToEnd + const isTruncated = jc >= perPage && !lastFetchWentToEnd return `${jc}${isTruncated ? '+' : ''} job${jc != 1 ? 's' : ''}` } @@ -338,13 +340,15 @@ {/snippet} {#snippet footer()}
{#if !lastFetchWentToEnd && jobs && jobs.length >= 1000} + >{#if !lastFetchWentToEnd && jobs && jobs.length >= perPage} + Load next {perPage} jobs + {/if}
{/snippet} diff --git a/frontend/src/lib/components/select/Select.svelte b/frontend/src/lib/components/select/Select.svelte index 287c620a7b..0327444bcf 100644 --- a/frontend/src/lib/components/select/Select.svelte +++ b/frontend/src/lib/components/select/Select.svelte @@ -1,4 +1,7 @@ - - - - { - const func = askingForConfirmation?.onConfirm - await func?.(forceCancelInPopup) - askingForConfirmation = undefined - }} - type={askingForConfirmation?.type} - loading={askingForConfirmation?.loading} - on:canceled={() => { - askingForConfirmation = undefined - }} -> - {#if askingForConfirmation?.preContent} -
{askingForConfirmation.preContent}
- - {#if forceCancelInPopup} -
-

- Force cancel is enabled. This is dangerous, only do this if you have no alternatives. - Instead of being gracefully cancelled, all jobs will be immediately sent to the completed - job table regardless of them being processed or not or part of running flows. You may end - up in an inconsistent state. -

-
- {/if} - {/if} -
- - - - {#if selectedIds.length === 1} - {#if selectedIds[0] === '-'} -
There is no information available for this job
- {:else} - - {/if} - {/if} -
-
- - { - reset() - loadFromQuery() - }} -/> - -{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.runs} - -{:else} -
- -
-
-
-

- Runs -

- - - All past and schedule executions of scripts and flows, including previews. You only see - your own runs or runs of groups you belong to unless you are an admin. - -
- - - { - jobsFilter('waiting') - }} - onJobsSuspended={() => { - jobsFilter('suspended') - }} - small={innerWidth < smallScreenWidth} - /> -
- -
- -
- - {#if minTs || maxTs} - - {/if} - { - minTs = new Date(detail).toISOString() - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - on:clear={async () => { - minTs = undefined - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - /> - - - - {#if maxTs || minTs} - - {/if} - { - maxTs = new Date(detail).toISOString() - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - on:clear={async () => { - maxTs = undefined - calendarChangeTimeout && clearTimeout(calendarChangeTimeout) - calendarChangeTimeout = setTimeout(() => { - jobsLoader?.loadJobs(minTs, maxTs, true) - }, 1000) - }} - /> - - - {#if minTs || maxTs} - - - - {/if} -
- - -
- { - if (e.detail == 'running' && maxTs != undefined) { - maxTs = undefined - } - }} - {usernames} - {folders} - {paths} - mobile={innerWidth < verySmallScreenWidth} - small={innerWidth < smallScreenWidth} - calendarSmall={!minTs && !maxTs} - /> -
-
-
- - -
-
-
- { - graph = detail - graphIsRunsChart = graph === 'RunChart' - }} - > - {#snippet children({ item })} - - - {/snippet} - - - {#if !graphIsRunsChart} - setLookback(0), - id: '0' - }, - { - displayName: '1 day', - action: () => setLookback(1), - id: '1' - }, - { - displayName: '3 days', - action: () => setLookback(3), - id: '3' - }, - { - displayName: '7 days', - action: () => setLookback(7), - id: '7' - } - ]} - selected={lookback.toString()} - selectedDisplayName={`${lookback} days lookback`} - > - {#snippet extraLabel()} - - {#snippet text()} - How far behind the min datetime to start considering jobs for the concurrency - graph. Change this value to include jobs started before the set time window for - the computation of the graph - {/snippet} - - {/snippet} - - {/if} -
-
- {#if graph === 'RunChart'} - { - minTs = zoom.min.toISOString() - maxTs = zoom.max.toISOString() - manualDatePicker?.resetChoice() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - onPointClicked={(ids) => { - runsTable?.scrollToRun(ids) - }} - /> - {:else if graph === 'ConcurrencyChart'} - { - minTs = zoom.min.toISOString() - maxTs = zoom.max.toISOString() - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - /> - {/if} -
- -
- - -
- -
-
- {#if selectionMode && selectableJobCount} -
-
- -
- -
- {/if} - - -
- -
-
- { - localStorage.setItem( - 'show_schedules_in_run', - showSchedules ? 'true' : 'false' - ) - }} - options={tableTopBarWidth < 800 || selectionMode - ? {} - : { right: 'Cron schedules' }} - /> - - - -
- -
- { - localStorage.setItem('show_future_jobs', showFutureJobs ? 'true' : 'false') - }} - id="planned-later" - options={tableTopBarWidth < 800 || selectionMode - ? {} - : { right: 'Planned later' }} - /> - - - -
-
- { - lastFetchWentToEnd = false - jobsLoader?.loadJobs(minTs, maxTs, true) - }} - bind:minTs - bind:maxTs - bind:selectedManualDate - {loading} - bind:this={manualDatePicker} - /> - { - localStorage.setItem('auto_refresh_in_runs', autoRefresh ? 'true' : 'false') - }} - options={{ right: 'Auto-refresh' }} - textClass="whitespace-nowrap" - /> -
-
-
- - -
- {#if jobs} - - {:else} -
- {#each new Array(8) as _} - - {/each} -
- {/if} -
-
-
- 0}> - {#if selectionMode === 're-run'} - - {:else if selectedIds.length === 1} - {#if selectedIds[0] === '-'} -
There is no information available for this job
- {:else} - - {/if} - {:else if selectedIds.length > 1} -
There are {selectedIds.length} jobs selected. Choose 1 to see detailed information
- {/if} -
-
-
-
-{/if} +{#key perPage} + +{/key} diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index cc7d9e67b4..76f22642bd 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -791,7 +791,7 @@ {#if script} import { BROWSER } from 'esm-env' - import { base } from '$lib/base' - import AppPreview from '$lib/components/apps/editor/AppPreview.svelte' - import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types' - import { Alert, Skeleton } from '$lib/components/common' - import { WindmillIcon } from '$lib/components/icons' import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen' - import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores' - import { twMerge } from 'tailwind-merge' + import { userStore, workspaceStore } from '$lib/stores' import { setContext } from 'svelte' - import { writable } from 'svelte/store' import { setLicense } from '$lib/enterpriseUtils' - import { isCloudHosted } from '$lib/cloud' - import Login from '$lib/components/Login.svelte' + import { getUserExt } from '$lib/user' - import { User, UserRoundX } from 'lucide-svelte' - import { goto } from '$app/navigation' import { sendUserToast } from '$lib/toast' import { page } from '$app/state' - import { urlParamsToObject } from '$lib/utils' + import PublicApp from '$lib/components/apps/editor/PublicApp.svelte' let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined) let notExists = $state(false) let noPermission = $state(false) - let jwtError = $state(false) - setContext(IS_APP_PUBLIC_CONTEXT_KEY, true) function isJwt(t: string) { // simply check that the first part is a valid base64 encoded json @@ -56,9 +44,10 @@ } } - const parsedCustomPath = parseCustomPath(page.params.path ?? '') - + let workspace: string | undefined = $state(undefined) async function loadApp() { + const parsedCustomPath = parseCustomPath(page.params.path ?? '') + if (parsedCustomPath.jwt) { const token = 'jwt_ext_' + parsedCustomPath.jwt OpenAPI.TOKEN = token @@ -69,6 +58,7 @@ app = await AppService.getPublicAppByCustomPath({ customPath: parsedCustomPath.path }) + workspace = app.workspace_id workspaceStore.set(app.workspace_id) noPermission = false notExists = false @@ -95,93 +85,15 @@ setLicense() loadApp() } - - const breakpoint = writable('lg') - - const darkMode = - window.localStorage.getItem('dark-mode') ?? - (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') - - if (darkMode === 'dark') { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - - -
{#if $userStore} -
{$userStore.username}
- {:else}{/if} -
- -{#if notExists} -
There was an error loading the app, is the url correct? Go to Windmill -
-{:else if noPermission} -
{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged - in and have read access to this app{/if}
-
- {#if !jwtError} - { - - // window.location.reload() - loadApp() - app = app - }} - popup - rd={page.url.toString()} - /> - {/if} -
-{:else if app} - {#key app} -
- goto(path)} - gotoFn={(path, opt) => goto(path, opt)} - /> -
- {/key} -{:else} - -{/if} + { + loadApp() + }} +> diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback/[client_name]/+page.svelte b/frontend/src/routes/oauth/callback/[client_name]/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback/[client_name]/+page.svelte rename to frontend/src/routes/oauth/callback/[client_name]/+page.svelte diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback_slack/+page.svelte b/frontend/src/routes/oauth/callback_slack/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback_slack/+page.svelte rename to frontend/src/routes/oauth/callback_slack/+page.svelte diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback_slack/instance/+page.svelte b/frontend/src/routes/oauth/callback_slack/instance/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback_slack/instance/+page.svelte rename to frontend/src/routes/oauth/callback_slack/instance/+page.svelte diff --git a/frontend/src/routes/(root)/(logged)/oauth/callback_supabase/+page.svelte b/frontend/src/routes/oauth/callback_supabase/+page.svelte similarity index 100% rename from frontend/src/routes/(root)/(logged)/oauth/callback_supabase/+page.svelte rename to frontend/src/routes/oauth/callback_supabase/+page.svelte diff --git a/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte b/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte index fadae2baf9..c661aa48c6 100644 --- a/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte +++ b/frontend/src/routes/public/[workspace]/[...secret]/+page.svelte @@ -1,34 +1,22 @@ - - -
{#if $userStore} -
{$userStore.username}
- {:else}{/if} -
- -{#if notExists} -
There was an error loading the app, is the url correct? Go to Windmill -
-{:else if noPermission} -
{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged - in and have read access to this app{/if}
-
- {#if !jwtError} - { - console.log('login') - // window.location.reload() - loadUser().then(() => { - loadApp() - }) - app = app - }} - popup - rd={page.url.toString()} - /> - {/if} -
-{:else if app} - {#key app} -
- goto(path)} - gotoFn={(path, opt) => goto(path, opt)} - /> -
- {/key} -{:else} - -{/if} + { + loadAll() + }} +> diff --git a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte index 902a9e4a15..ac05ae04b5 100644 --- a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte +++ b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte @@ -12,6 +12,8 @@ import { parseQueryParams } from '$lib/utils' import { page } from '$app/state' import { isCloudHosted } from '$lib/cloud' + import { getCookie } from '$lib/cookies' + // import { getAndDeleteCookie } from '$lib/cookies' let error = page.url.searchParams.get('error') let clientName = page.params.client_name ?? '' @@ -19,11 +21,14 @@ let state = page.url.searchParams.get('state') ?? undefined onMount(async () => { + // const closeCookie = getAndDeleteCookie('close') + // console.log('closeCookie', closeCookie) const rd = localStorage.getItem('rd') if (rd) { localStorage.removeItem('rd') } - const closeUponLogin = localStorage.getItem('closeUponLogin') == 'true' + const cookieCloseUponLogin = getCookie('close') == 'true' + const closeUponLogin = cookieCloseUponLogin ?? localStorage.getItem('closeUponLogin') == 'true' if (error) { sendUserToast(`Error trying to login with ${clientName} ${error}`, true) if (closeUponLogin) { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 9fc6660220..522a6be139 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -11,7 +11,15 @@ const version = JSON.parse(json) const config = { server: { https: process.env.HTTPS === 'true', - allowedHosts: ['localhost', '127.0.0.1', '0.0.0.0', 'rubendev.wimill.xyz', 'windmill.xyz'], + allowedHosts: [ + 'localhost', + '127.0.0.1', + '0.0.0.0', + 'rubendev.wimill.xyz', + 'windmill.xyz', + 'app.windmill.xyz', + 'public.windmill.xyz' + ], port: 3000, proxy: { '^/api/w/[^/]+/s3_proxy/.*': { @@ -64,7 +72,7 @@ const config = { exclude: [ '@codingame/monaco-vscode-standalone-typescript-language-features', '@codingame/monaco-vscode-standalone-languages' - ], + ] }, worker: { format: 'es' diff --git a/lsp/Pipfile b/lsp/Pipfile index 8e69ddb427..fb1c0f45a2 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.575.0" -wmill_pg = ">=1.575.0" +wmill = ">=1.575.4" +wmill_pg = ">=1.575.4" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index b3dfd7c8f4..5e3d9dad11 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.575.0 + version: 1.575.4 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/powershell-client/WindmillClient/WindmillClient.psd1 b/powershell-client/WindmillClient/WindmillClient.psd1 index b75c5d2e72..a843ee48ad 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.575.0' + ModuleVersion = '1.575.4' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index c4d9c9329e..25994859cc 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.575.0" +version = "1.575.4" 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 b148a05a68..148cb808dd 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.575.0" +version = "1.575.4" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/typescript-client/jsr.json b/typescript-client/jsr.json index 564bf9b984..b2c821e8ff 100644 --- a/typescript-client/jsr.json +++ b/typescript-client/jsr.json @@ -1,6 +1,6 @@ { "name": "@windmill/windmill", - "version": "1.575.0", + "version": "1.575.4", "exports": "./src/index.ts", "publish": { "exclude": ["!src", "./s3Types.ts", "./client.ts"] diff --git a/typescript-client/package.json b/typescript-client/package.json index 1efa54ef76..3f178725a7 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.575.0", + "version": "1.575.4", "author": "Ruben Fiszel", "license": "Apache 2.0", "devDependencies": { diff --git a/version.txt b/version.txt index f227cd375d..b465773b4f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.575.0 +1.575.4