Merge remote-tracking branch 'origin/main' into glm/frontend/add_notes_to_flow

# Conflicts:
#	backend/windmill-api/src/flows.rs
This commit is contained in:
Guilhem
2025-11-17 08:38:42 +00:00
108 changed files with 4313 additions and 2736 deletions
+9 -1
View File
@@ -67,13 +67,21 @@ jobs:
- name: Substitute EE code (EE logic is behind feature flag)
run: |
./substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Cache DuckDB FFI module build
uses: actions/cache@v3
with:
path: ./backend/windmill-duckdb-ffi-internal/target
key: ${{ runner.os }}-duckdb-ffi-${{ hashFiles('./backend/windmill-duckdb-ffi-internal/src/**/*.rs', './backend/windmill-duckdb-ffi-internal/Cargo.toml', './backend/windmill-duckdb-ffi-internal/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-duckdb-ffi-
- name: cargo test
timeout-minutes: 16
run: deno --version && bun -v && go version && python3 --version &&
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd .. &&
SQLX_OFFLINE=true
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
DISABLE_EMBEDDING=true RUST_LOG=info RUST_LOG_STYLE=never
DENO_PATH=$(which deno) BUN_PATH=$(which bun) GO_PATH=$(which go)
UV_PATH=$(which uv) cargo test --features
enterprise,deno_core,license,python,rust,scoped_cache,private --all --
enterprise,deno_core,license,python,duckdb,rust,scoped_cache,private --all --
--nocapture
+65
View File
@@ -1,5 +1,70 @@
# Changelog
## [1.576.3](https://github.com/windmill-labs/windmill/compare/v1.576.2...v1.576.3) (2025-11-15)
### Bug Fixes
* handle better alias types in duckdb ([2c04e04](https://github.com/windmill-labs/windmill/commit/2c04e04bf0e3272c89f321392158888d02a1191b))
## [1.576.2](https://github.com/windmill-labs/windmill/compare/v1.576.1...v1.576.2) (2025-11-15)
### Bug Fixes
* temporary fix for duckdb type_aliases causing issues ([#7148](https://github.com/windmill-labs/windmill/issues/7148)) ([6426ebf](https://github.com/windmill-labs/windmill/commit/6426ebf8cb713443904065064b6a07eb1db0761a))
## [1.576.1](https://github.com/windmill-labs/windmill/compare/v1.576.0...v1.576.1) (2025-11-14)
### Bug Fixes
* DuckDB FFI crash fix ([#7145](https://github.com/windmill-labs/windmill/issues/7145)) ([d3fc459](https://github.com/windmill-labs/windmill/commit/d3fc459b407682bf588236236916363d94f3e1ff))
## [1.576.0](https://github.com/windmill-labs/windmill/compare/v1.575.4...v1.576.0) (2025-11-14)
### Features
* add support for switch and attributes in pwsh params ([#7143](https://github.com/windmill-labs/windmill/issues/7143)) ([c16bef8](https://github.com/windmill-labs/windmill/commit/c16bef8f296645ff873f9d8d28e3dcb50a65e304))
* **ai:** handle aws bedrock as provider ([#7131](https://github.com/windmill-labs/windmill/issues/7131)) ([30eb9aa](https://github.com/windmill-labs/windmill/commit/30eb9aae25eeb563ad119ef93f3ff1ab17c66d75))
* webhook by flow version ([#7062](https://github.com/windmill-labs/windmill/issues/7062)) ([09cdfb4](https://github.com/windmill-labs/windmill/commit/09cdfb4556748903dc5bbf53ef3356ac97c57d90))
### Bug Fixes
* use proper TLS connector for DuckLake instance catalog setup ([#7138](https://github.com/windmill-labs/windmill/issues/7138)) ([cf36fe3](https://github.com/windmill-labs/windmill/commit/cf36fe3bb1beec80fa84dc342a8a38cc7369bc4d))
## [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)
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme', $2, now()) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "cccf9e216d84dfbb801b92d697496f44d93b344190a26c8393867b3a268b9aba"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+79 -78
View File
@@ -199,22 +199,22 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.4"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -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",
@@ -1026,7 +1026,7 @@ dependencies = [
"http 1.3.1",
"http-body 0.4.6",
"hyper 0.14.32",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.24.2",
"hyper-rustls 0.27.7",
"hyper-util",
@@ -1182,7 +1182,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"itoa",
"matchit",
@@ -1569,7 +1569,7 @@ dependencies = [
"hex",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-named-pipe",
"hyper-util",
"hyperlocal",
@@ -1799,9 +1799,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
dependencies = [
"serde",
]
@@ -1945,9 +1945,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.45"
version = "1.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -3625,7 +3625,7 @@ dependencies = [
"hickory-resolver",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"ipnet",
@@ -3713,7 +3713,7 @@ dependencies = [
"http 1.3.1",
"httparse",
"hyper 0.14.32",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"itertools 0.10.5",
"memmem",
@@ -3906,7 +3906,7 @@ dependencies = [
"hkdf",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"idna",
"indexmap 2.11.1",
@@ -4177,7 +4177,7 @@ dependencies = [
"http 1.3.1",
"http-body-util",
"hyper 0.14.32",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"libc",
"log",
@@ -4231,7 +4231,7 @@ dependencies = [
"deno_error",
"deno_tls",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"log",
@@ -4361,7 +4361,7 @@ dependencies = [
"h2 0.4.12",
"http 1.3.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"once_cell",
"rustls-tokio-stream",
@@ -5225,7 +5225,7 @@ dependencies = [
"base64 0.21.7",
"bytes",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project",
"rand 0.8.5",
@@ -5292,9 +5292,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fixedbitset"
@@ -6604,9 +6604,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
@@ -6635,7 +6635,7 @@ dependencies = [
"futures-util",
"headers",
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-util",
"pin-project-lite",
@@ -6652,7 +6652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
dependencies = [
"hex",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -6683,7 +6683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"log",
"rustls 0.23.29",
@@ -6701,7 +6701,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -6729,7 +6729,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"native-tls",
"tokio",
@@ -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",
@@ -6750,7 +6750,7 @@ dependencies = [
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"hyper 1.8.0",
"hyper 1.8.1",
"ipnet",
"libc",
"percent-encoding",
@@ -6771,7 +6771,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
"hex",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -7419,7 +7419,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-http-proxy",
"hyper-rustls 0.27.7",
"hyper-timeout",
@@ -8946,7 +8946,7 @@ dependencies = [
"http-body-util",
"httparse",
"humantime",
"hyper 1.8.0",
"hyper 1.8.1",
"itertools 0.14.0",
"md-5 0.10.6",
"parking_lot 0.12.5",
@@ -10712,7 +10712,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-rustls 0.27.7",
"hyper-tls 0.6.0",
"hyper-util",
@@ -10772,7 +10772,7 @@ dependencies = [
"futures",
"getrandom 0.2.16",
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"parking_lot 0.11.2",
"reqwest 0.12.24",
"reqwest-middleware",
@@ -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",
@@ -13945,7 +13945,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.8.0",
"hyper 1.8.1",
"hyper-timeout",
"hyper-util",
"percent-encoding",
@@ -15148,7 +15148,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15208,7 +15208,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"argon2",
@@ -15251,7 +15251,7 @@ dependencies = [
"hf-hub",
"hmac",
"http 1.3.1",
"hyper 1.8.0",
"hyper 1.8.1",
"indexmap 2.11.1",
"itertools 0.14.0",
"jsonwebtoken 8.3.0",
@@ -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.576.3"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15343,7 +15344,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"chrono",
"lazy_static",
@@ -15357,7 +15358,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"axum",
@@ -15376,7 +15377,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"async-recursion",
@@ -15404,7 +15405,7 @@ dependencies = [
"globset",
"hex",
"hmac",
"hyper 1.8.0",
"hyper 1.8.1",
"indexmap 2.11.1",
"itertools 0.14.0",
"jsonwebtoken 8.3.0",
@@ -15461,7 +15462,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"regex",
"serde",
@@ -15476,7 +15477,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"bytes",
@@ -15500,7 +15501,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15512,7 +15513,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15521,7 +15522,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15533,7 +15534,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"serde_json",
@@ -15545,7 +15546,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"gosyn",
@@ -15557,7 +15558,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15569,7 +15570,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"serde_json",
@@ -15581,7 +15582,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"nu-parser",
@@ -15592,7 +15593,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15603,7 +15604,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15615,7 +15616,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"async-recursion",
@@ -15638,7 +15639,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15652,7 +15653,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15669,7 +15670,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15683,7 +15684,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"lazy_static",
@@ -15701,7 +15702,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"serde",
@@ -15712,7 +15713,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"anyhow",
"async-recursion",
@@ -15747,7 +15748,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.575.0"
version = "1.576.3"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15757,7 +15758,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.575.0"
version = "1.576.3"
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]]
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.575.0"
version = "1.576.3"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.575.0"
version = "1.576.3"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
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"
+1 -1
View File
@@ -1 +1 @@
7cdb62d968feef22046b12a1217b711af457cced
423c3c2d175b15b7c0f010e08f1848a7710e31be
+518 -103
View File
@@ -46,8 +46,6 @@ pub fn parse_powershell_sig(code: &str) -> anyhow::Result<MainArgSignature> {
lazy_static::lazy_static! {
static ref RE_BASH: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+)\}|\{(\d+):-(.*)\})"(?:[\t ]*)?(?:#.*)?\r?$"#).unwrap();
static ref RE_POWERSHELL_ARGS: Regex = Regex::new(r#"(?:\[([\w\[\]]+)\])?\$(\w+)[\t ]*(?:=[\t ]*(?:(?:(?:"|')([^"\n\r\$]*)(?:"|'))|([\d.]+)))?\r?"#).unwrap();
}
fn parse_bash_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
@@ -98,10 +96,12 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
let lower_code = code.to_lowercase();
let param_start = lower_code.find("param")?;
// Verify that only comments and whitespace appear before "param"
// Verify that only comments, whitespace, and [CmdletBinding()] appear before "param"
let before_param = &code[..param_start];
let mut chars = before_param.chars().peekable();
let mut in_block_comment = false;
let mut in_attribute_bracket = false;
let mut bracket_depth = 0;
while let Some(ch) = chars.next() {
if in_block_comment {
@@ -110,6 +110,19 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
chars.next(); // consume '>'
in_block_comment = false;
}
} else if in_attribute_bracket {
// Track bracket depth to handle nested brackets/parens in attributes
match ch {
'[' => bracket_depth += 1,
']' => {
bracket_depth -= 1;
if bracket_depth == 0 {
in_attribute_bracket = false;
}
}
// Allow parentheses inside attributes (e.g., [CmdletBinding()])
_ => {}
}
} else {
match ch {
// Start of block comment: <#
@@ -126,6 +139,11 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
chars.next();
}
}
// Start of attribute bracket (e.g., [CmdletBinding()])
'[' => {
in_attribute_bracket = true;
bracket_depth = 1;
}
// Whitespace is allowed
c if c.is_whitespace() => {}
// Any other character means there's code before param
@@ -134,8 +152,8 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
}
}
// If we're still in a block comment at the end, it's unclosed - invalid
if in_block_comment {
// If we're still in a block comment or unclosed attribute bracket, it's invalid
if in_block_comment || in_attribute_bracket {
return None;
}
@@ -214,108 +232,269 @@ pub fn extract_powershell_param_block(code: &str, include_keyword: bool) -> Opti
None
}
enum ParserState {
Normal,
InSingleQuote,
InDoubleQuote,
}
fn split_pwsh_args(code: &str) -> Vec<&str> {
let mut chars = code.char_indices().peekable();
let mut state = ParserState::Normal;
let mut splits = vec![];
let mut last_idx = 0;
while let Some((idx, char)) = chars.next() {
match (&state, char) {
(ParserState::Normal, '\'') => {
state = ParserState::InSingleQuote;
}
(ParserState::Normal, '"') => {
state = ParserState::InDoubleQuote;
}
(ParserState::InSingleQuote, '\'') => {
state = ParserState::Normal;
}
(ParserState::InDoubleQuote, '"') => {
state = ParserState::Normal;
}
(ParserState::Normal, ',') => {
splits.push(&code[last_idx..idx]);
last_idx = idx + 1; // skip the comma
}
_ => {}
}
}
if last_idx < code.len() {
splits.push(&code[last_idx..]);
}
splits
}
fn parse_powershell_single_typ(typ: &str) -> Typ {
match typ.to_lowercase().as_str() {
"string" => Typ::Str(None),
"int" | "long" => Typ::Int,
"decimal" | "double" | "single" => Typ::Float,
"datetime" => Typ::Datetime,
"bool" => Typ::Bool,
"bool" | "switch" => Typ::Bool,
"pscustomobject" => Typ::Object(ObjectType::new(None, None)),
_ => Typ::Str(None),
}
}
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let param_wrapper = extract_powershell_param_block(code, false);
let mut args = vec![];
if let Some(param_wrapper) = param_wrapper {
let params = split_pwsh_args(param_wrapper);
for param in params {
if let Some(cap) = RE_POWERSHELL_ARGS.captures(param) {
let typ = cap.get(1).map(|x| x.as_str().to_string());
let name = cap.get(2).unwrap().as_str().to_string();
/// Single-pass PowerShell parameter parser.
/// Parses the content of a param() block and extracts all parameter information.
///
/// This function processes PowerShell parameter declarations in a single pass, handling:
/// - Parameter attributes: [Parameter(Mandatory)], [Parameter(Mandatory=$true)], [ValidateSet(...)], etc.
/// - Type annotations: [string], [int[]], [PSCustomObject], etc.
/// - Variable names: $Name, $Value, etc.
/// - Default values: = 'text', = 25, = $env:VAR, etc.
/// - Mandatory detection: Parameters with Mandatory attribute are marked as required
fn parse_powershell_parameters(content: &str) -> anyhow::Result<Vec<Arg>> {
#[derive(Debug, PartialEq)]
enum State {
Normal,
InSingleQuote,
InDoubleQuote,
InBracket,
}
let mut parsed_typ = if let Some(typ) = typ {
if typ.as_str().ends_with("[]") {
Some(Typ::List(Box::new(parse_powershell_single_typ(
typ.as_str().strip_suffix("[]").unwrap(),
))))
} else {
Some(parse_powershell_single_typ(typ.as_str()))
let mut args = Vec::new();
let mut chars = content.char_indices().peekable();
let mut state = State::Normal;
let mut bracket_depth: i32 = 0;
let mut paren_depth: i32 = 0;
// Current parameter being built
let mut type_annotation: Option<String> = None;
let mut var_name: Option<String> = None;
let mut default_value: Option<String> = None;
let mut is_mandatory = false;
// Track position for extracting text
let mut last_bracket_start = None;
let mut found_dollar = false;
while let Some((idx, ch)) = chars.next() {
match state {
State::InSingleQuote => {
if ch == '\'' {
state = State::Normal;
}
}
State::InDoubleQuote => {
if ch == '"' {
// Check for escape character
if idx > 0 && content.chars().nth(idx - 1) != Some('`') {
state = State::Normal;
}
} else {
None
};
}
}
State::InBracket => {
match ch {
'[' => bracket_depth += 1,
']' => {
bracket_depth -= 1;
if bracket_depth == 0 {
// Extract the bracket content
if let Some(start) = last_bracket_start {
let bracket_content = &content[start + 1..idx];
let default = if let Some(x) = cap.get(3) {
Some(json!(x.as_str().to_string()))
} else if let Some(x) = cap.get(4) {
if parsed_typ.is_none() {
if x.as_str().parse::<i64>().is_ok() {
parsed_typ = Some(Typ::Int);
} else if x.as_str().parse::<f64>().is_ok() {
parsed_typ = Some(Typ::Float);
// Check if this is a Parameter attribute with Mandatory (case-insensitive)
let lower = bracket_content.to_lowercase();
if lower.starts_with("parameter(") || lower.starts_with("parameter ") {
// Check for Mandatory (case-insensitive)
if lower.contains("mandatory") {
// Check if it's explicitly set to false
if !lower.contains("mandatory=$false") && !lower.contains("mandatory = $false") {
is_mandatory = true;
}
}
}
// Check if this looks like a type (simple word, possibly with [])
let is_type = !bracket_content.contains('(')
&& !bracket_content.contains('=')
&& (bracket_content.chars().next().unwrap_or(' ').is_alphabetic()
|| bracket_content.starts_with('['));
if is_type && !found_dollar {
type_annotation = Some(bracket_content.to_string());
}
}
state = State::Normal;
last_bracket_start = None;
}
}
serde_json::Number::from_str(x.as_str())
.ok()
.map(serde_json::Value::Number)
} else {
None
};
'(' => paren_depth += 1,
')' => paren_depth = paren_depth.saturating_sub(1),
_ => {}
}
}
State::Normal => {
match ch {
'\'' => state = State::InSingleQuote,
'"' => state = State::InDoubleQuote,
'[' => {
state = State::InBracket;
bracket_depth = 1;
last_bracket_start = Some(idx);
}
'$' => {
found_dollar = true;
// Extract variable name
let name_start = idx + 1;
let mut name_end = name_start;
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch.is_alphanumeric() || next_ch == '_' {
name_end += 1;
chars.next();
} else {
break;
}
}
var_name = Some(content[name_start..name_end].to_string());
}
'=' if found_dollar => {
// Extract default value
// Skip whitespace after =
while let Some(&(_, next_ch)) = chars.peek() {
if next_ch.is_whitespace() {
chars.next();
} else {
break;
}
}
args.push(Arg {
name: name,
typ: parsed_typ.unwrap_or(Typ::Str(None)),
default: default.clone(),
otyp: None,
has_default: default.is_some(),
oidx: None,
});
let default_start = chars.peek().map(|(i, _)| *i).unwrap_or(content.len());
let mut default_end = default_start;
let mut in_string = false;
let mut string_char = ' ';
while let Some((i, ch)) = chars.peek().copied() {
if in_string {
if ch == string_char && content.chars().nth(i.saturating_sub(1)) != Some('`') {
in_string = false;
default_end = i + 1;
chars.next();
} else {
default_end = i + 1;
chars.next();
}
} else if ch == '\'' || ch == '"' {
in_string = true;
string_char = ch;
default_end = i + 1;
chars.next();
} else if ch == ',' {
break;
} else if ch.is_whitespace() && chars.clone().skip(1).next().map(|(_, c)| c) == Some(',') {
break;
} else {
default_end = i + 1;
chars.next();
}
}
default_value = Some(content[default_start..default_end].trim().to_string());
}
',' => {
// End of parameter, finalize it
if let Some(name) = var_name.take() {
args.push(finalize_parameter(name, type_annotation.take(), default_value.take(), is_mandatory)?);
}
// Reset for next parameter
type_annotation = None;
var_name = None;
default_value = None;
is_mandatory = false;
found_dollar = false;
}
_ => {}
}
}
}
}
Ok(Some(args))
// Finalize last parameter
if let Some(name) = var_name {
args.push(finalize_parameter(name, type_annotation, default_value, is_mandatory)?);
}
Ok(args)
}
fn finalize_parameter(
name: String,
type_annotation: Option<String>,
default_value: Option<String>,
is_mandatory: bool,
) -> anyhow::Result<Arg> {
// Store the original PowerShell type for use in the executor
let otyp = type_annotation.clone();
let mut parsed_typ = if let Some(typ) = type_annotation {
if typ.ends_with("[]") {
Some(Typ::List(Box::new(parse_powershell_single_typ(
typ.strip_suffix("[]").unwrap(),
))))
} else {
Some(parse_powershell_single_typ(&typ))
}
} else {
None
};
let default = if let Some(default_str) = default_value {
// Try to parse as string (quoted)
if (default_str.starts_with('"') && default_str.ends_with('"'))
|| (default_str.starts_with('\'') && default_str.ends_with('\''))
{
Some(json!(default_str[1..default_str.len() - 1].to_string()))
} else {
// Try to parse as number
if parsed_typ.is_none() {
if default_str.parse::<i64>().is_ok() {
parsed_typ = Some(Typ::Int);
} else if default_str.parse::<f64>().is_ok() {
parsed_typ = Some(Typ::Float);
}
}
serde_json::Number::from_str(&default_str)
.ok()
.map(serde_json::Value::Number)
}
} else {
None
};
// has_default semantics:
// - true: parameter is optional (has a default value OR is not mandatory)
// - false: parameter is required (marked as Mandatory AND no default value)
// Simplified: A parameter is optional unless it's mandatory without a default
let has_default = default.is_some() || !is_mandatory;
Ok(Arg {
name,
typ: parsed_typ.unwrap_or(Typ::Str(None)),
default: default.clone(),
otyp,
has_default,
oidx: None,
})
}
fn parse_powershell_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let param_wrapper = extract_powershell_param_block(code, false);
if let Some(param_wrapper) = param_wrapper {
Ok(Some(parse_powershell_parameters(param_wrapper)?))
} else {
Ok(Some(vec![]))
}
}
#[cfg(test)]
@@ -402,23 +581,23 @@ non_required="${5:-}"
star_kwargs: false,
args: vec![
Arg {
otyp: None,
otyp: None, // No type annotation
name: "Msg".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("string".to_string()), // [string]
name: "Msg2".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: None, // No type annotation
name: "Dflt".to_string(),
typ: Typ::Str(None),
default: Some(json!("default value, with comma")),
@@ -426,7 +605,7 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: Some("int".to_string()), // [int]
name: "Nb".to_string(),
typ: Typ::Int,
default: Some(json!(3)),
@@ -434,7 +613,7 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: None, // Type inferred from default value
name: "Nb2".to_string(),
typ: Typ::Float,
default: Some(json!(5.0)),
@@ -442,7 +621,7 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: None, // Type inferred from default value
name: "Nb3".to_string(),
typ: Typ::Int,
default: Some(json!(5)),
@@ -450,35 +629,35 @@ non_required="${5:-}"
oidx: None
},
Arg {
otyp: None,
otyp: None, // No type annotation
name: "Wahoo".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("PSCustomObject".to_string()), // [PSCustomObject]
name: "Obj".to_string(),
typ: Typ::Object(ObjectType::new(None, None)),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("string[]".to_string()), // [string[]]
name: "Arr".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false,
has_default: true, // Optional (not mandatory)
oidx: None
},
Arg {
otyp: None,
otyp: Some("string".to_string()), // [string] (last type bracket with Mandatory)
name: "Message".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false,
has_default: false, // Required (Mandatory attribute)
oidx: None
}
],
@@ -602,6 +781,242 @@ non_required="${5:-}"
extract_powershell_param_block("function test-x{ param($Name)\n}", false),
None
);
// Valid: [CmdletBinding()] before param
assert_eq!(
extract_powershell_param_block("[CmdletBinding()]\nparam($Name)", false),
Some("$Name")
);
assert_eq!(
extract_powershell_param_block("[CmdletBinding()]\nparam($Name, $Age)", true),
Some("param($Name, $Age)")
);
// Valid: [CmdletBinding()] with options before param
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding(SupportsShouldProcess=$true)]\nparam($Path)",
false
),
Some("$Path")
);
// Valid: Multiple attributes before param
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding()]\n[OutputType([string])]\nparam($Value)",
false
),
Some("$Value")
);
// Valid: CmdletBinding with comments
assert_eq!(
extract_powershell_param_block(
"# My function\n[CmdletBinding()]\nparam($Name)",
false
),
Some("$Name")
);
// Valid: CmdletBinding with whitespace variations
assert_eq!(
extract_powershell_param_block(
"[CmdletBinding()] \n param($Name)",
false
),
Some("$Name")
);
// Invalid: Unclosed attribute bracket
assert_eq!(
extract_powershell_param_block("[CmdletBinding(\nparam($Name)", false),
None
);
}
#[test]
fn test_parse_powershell_sig_with_parameter_attributes() -> anyhow::Result<()> {
// Test with [Parameter(Mandatory=$true)] attribute
let code = r#"[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[string]$Name,
[Parameter(Mandatory=$false)]
[int]$Age = 25
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 2);
assert_eq!(result.args[0].name, "Name");
assert_eq!(result.args[0].typ, Typ::Str(None));
assert_eq!(result.args[0].has_default, false);
assert_eq!(result.args[1].name, "Age");
assert_eq!(result.args[1].typ, Typ::Int);
assert_eq!(result.args[1].has_default, true);
assert_eq!(result.args[1].default, Some(json!(25)));
// Test with complex attributes
let code2 = r#"param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateSet('Red', 'Green', 'Blue')]
[string]$Color,
[Parameter(ValueFromPipeline=$true)]
[string[]]$Items
)"#;
let result2 = parse_powershell_sig(code2)?;
assert_eq!(result2.args.len(), 2);
assert_eq!(result2.args[0].name, "Color");
assert_eq!(result2.args[0].typ, Typ::Str(None));
assert_eq!(result2.args[1].name, "Items");
assert_eq!(result2.args[1].typ, Typ::List(Box::new(Typ::Str(None))));
Ok(())
}
#[test]
fn test_powershell_single_pass_parser() -> anyhow::Result<()> {
// Test the single-pass parser with a complex real-world example
let code = r#"[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0, HelpMessage="Enter the server name")]
[ValidateNotNullOrEmpty()]
[string]$ServerName,
[Parameter(Mandatory=$false)]
[ValidateRange(1, 65535)]
[int]$Port = 8080,
[Parameter(ValueFromPipeline=$true)]
[string[]]$LogFiles,
[ValidateSet('Debug', 'Info', 'Warning', 'Error')]
[string]$LogLevel = 'Info',
[PSCustomObject]$Config
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 5);
// ServerName: mandatory string with no default
assert_eq!(result.args[0].name, "ServerName");
assert_eq!(result.args[0].typ, Typ::Str(None));
assert_eq!(result.args[0].has_default, false);
// Port: optional int with default
assert_eq!(result.args[1].name, "Port");
assert_eq!(result.args[1].typ, Typ::Int);
assert_eq!(result.args[1].default, Some(json!(8080)));
assert_eq!(result.args[1].has_default, true);
// LogFiles: string array (no mandatory, so optional)
assert_eq!(result.args[2].name, "LogFiles");
assert_eq!(result.args[2].typ, Typ::List(Box::new(Typ::Str(None))));
assert_eq!(result.args[2].has_default, true); // Optional (not mandatory)
// LogLevel: string with default (ValidateSet is ignored but doesn't break parsing)
assert_eq!(result.args[3].name, "LogLevel");
assert_eq!(result.args[3].typ, Typ::Str(None));
assert_eq!(result.args[3].default, Some(json!("Info")));
assert_eq!(result.args[3].has_default, true);
// Config: PSCustomObject (no mandatory, so optional)
assert_eq!(result.args[4].name, "Config");
assert_eq!(result.args[4].typ, Typ::Object(ObjectType::new(None, None)));
assert_eq!(result.args[4].has_default, true); // Optional (not mandatory)
Ok(())
}
#[test]
fn test_powershell_mandatory_attribute() -> anyhow::Result<()> {
// Test various forms of the Mandatory attribute
let code = r#"param(
[Parameter(Mandatory)]
[string]$RequiredNoEquals,
[Parameter(Mandatory=$true)]
[string]$RequiredWithTrue,
[Parameter(Mandatory = $true)]
[string]$RequiredWithSpaces,
[Parameter(Mandatory=$false)]
[string]$NotRequired,
[Parameter(Position=0)]
[string]$NoMandatory,
[string]$PlainRequired = "default",
[Parameter(Mandatory=$true)]
[int]$RequiredInt
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 7);
// RequiredNoEquals: mandatory without =$true
assert_eq!(result.args[0].name, "RequiredNoEquals");
assert_eq!(result.args[0].has_default, false); // Required (mandatory, no default)
// RequiredWithTrue: mandatory with =$true
assert_eq!(result.args[1].name, "RequiredWithTrue");
assert_eq!(result.args[1].has_default, false); // Required
// RequiredWithSpaces: mandatory with spaces
assert_eq!(result.args[2].name, "RequiredWithSpaces");
assert_eq!(result.args[2].has_default, false); // Required
// NotRequired: explicitly Mandatory=$false
assert_eq!(result.args[3].name, "NotRequired");
assert_eq!(result.args[3].has_default, true); // Optional (not mandatory)
// NoMandatory: no Mandatory attribute
assert_eq!(result.args[4].name, "NoMandatory");
assert_eq!(result.args[4].has_default, true); // Optional (not mandatory)
// PlainRequired: has default value (always optional)
assert_eq!(result.args[5].name, "PlainRequired");
assert_eq!(result.args[5].has_default, true); // Optional (has default)
assert_eq!(result.args[5].default, Some(json!("default")));
// RequiredInt: mandatory int
assert_eq!(result.args[6].name, "RequiredInt");
assert_eq!(result.args[6].typ, Typ::Int);
assert_eq!(result.args[6].has_default, false); // Required
Ok(())
}
#[test]
fn test_powershell_case_insensitive_parameter() -> anyhow::Result<()> {
// Test that [parameter(...)] is case-insensitive
let code = r#"param(
[parameter(Mandatory)]
[string]$LowerCase,
[PARAMETER(MANDATORY=$TRUE)]
[string]$UpperCase,
[Parameter(mandatory=$true)]
[string]$MixedCase
)"#;
let result = parse_powershell_sig(code)?;
assert_eq!(result.args.len(), 3);
// All should be detected as mandatory
assert_eq!(result.args[0].name, "LowerCase");
assert_eq!(result.args[0].has_default, false);
assert_eq!(result.args[1].name, "UpperCase");
assert_eq!(result.args[1].has_default, false);
assert_eq!(result.args[2].name, "MixedCase");
assert_eq!(result.args[2].has_default, false);
Ok(())
}
#[test]
+2 -1
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# Run the command repeatedly until it fails (exits with non-zero code)
cd windmill-duckdb-ffi-internal && ./build_dev.sh && cd ..
while true; do
DISABLE_EMBEDDING=true \
RUST_LOG=info \
@@ -9,7 +10,7 @@ while true; do
GO_PATH=$(which go) \
UV_PATH=$(which uv) \
CARGO_PATH=$(which cargo) \
cargo test --features enterprise,deno_core,license,python,rust,scoped_cache \
cargo test --features enterprise,deno_core,license,python,duckdb,rust,scoped_cache \
-- --nocapture --test-threads=8 | tee /tmp/test.log
# Capture the exit code of the cargo test command (not tee)
+1 -1
View File
@@ -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
}
+47 -49
View File
@@ -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<Postgres>, base_internal_url: &str, node_n
);
}
timeouts
};
@@ -2253,7 +2252,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, 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<Postgres>, 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;
}
}
}
+1
View File
@@ -208,6 +208,7 @@ impl RunJob {
false,
None,
debounce_job_id_o,
None,
)
.await
.expect("push has to succeed");
+18 -9
View File
@@ -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::Value>(
&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::Value>(
&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();
+31 -1
View File
@@ -2769,7 +2769,7 @@ async fn test_result_format(db: Pool<Postgres>) -> 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",
)
@@ -2944,3 +2944,33 @@ async fn test_workflow_as_code(db: Pool<Postgres>) -> anyhow::Result<()> {
.await;
Ok(())
}
#[cfg(feature = "duckdb")]
#[sqlx::test(fixtures("base"))]
async fn test_duckdb_ffi(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let content = "-- result_collection=last_statement_first_row_scalar\nSELECT 'Hello world!';";
let flow: FlowValue = serde_json::from_value(serde_json::json!({
"modules": [{
"value": {
"type": "rawscript",
"language": "duckdb",
"content": content,
},
}],
}))
.unwrap();
let result =
RunJob::from(JobPayload::RawFlow { value: flow.clone(), path: None, restarted_from: None })
.run_until_complete(&db, false, server.addr.port())
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("Hello world!"));
Ok(())
}
+1
View File
@@ -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
+246 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.575.0
version: 1.576.3
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
+3
View File
@@ -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?;
@@ -222,6 +222,7 @@ async fn get_concurrent_intervals(
all_workspaces: _,
concurrency_key: Some(_),
allow_wildcards: None,
trigger_kind: _,
} => true,
_ => false,
};
+4 -1
View File
@@ -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"))
}
+40 -36
View File
@@ -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?;
@@ -1118,24 +1120,25 @@ async fn get_flow_by_path(
let flow_o = if query.with_starred_info.unwrap_or(false) {
sqlx::query_as::<_, FlowWithStarred>(
r#"
SELECT
flow.workspace_id,
flow.path,
flow.lock_error_logs,
flow.summary,
flow.description,
flow.archived,
flow.extra_perms,
flow.draft_only,
flow.dedicated_worker,
flow.tag,
flow.ws_error_handler_muted,
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow_version.schema,
flow_version.value,
flow_version.created_at AS edited_at,
SELECT
flow.workspace_id,
flow.path,
flow.lock_error_logs,
flow.summary,
flow.description,
flow.archived,
flow.extra_perms,
flow.draft_only,
flow.dedicated_worker,
flow.tag,
flow.ws_error_handler_muted,
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,
flow_version.created_by AS edited_by,
favorite.path IS NOT NULL AS starred
FROM flow
@@ -1157,25 +1160,26 @@ async fn get_flow_by_path(
} else {
sqlx::query_as::<_, FlowWithStarred>(
r#"
SELECT
flow.workspace_id,
flow.path,
flow.lock_error_logs,
flow.summary,
flow.description,
flow.archived,
flow.extra_perms,
flow.draft_only,
flow.dedicated_worker,
flow.tag,
flow.ws_error_handler_muted,
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow_version.schema,
SELECT
flow.workspace_id,
flow.path,
flow.lock_error_logs,
flow.summary,
flow.description,
flow.archived,
flow.extra_perms,
flow.draft_only,
flow.dedicated_worker,
flow.tag,
flow.ws_error_handler_muted,
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,
flow_version.created_by AS edited_by,
flow_version.created_at AS edited_at,
flow_version.created_by AS edited_by,
NULL AS starred
FROM flow
LEFT JOIN flow_version
+402 -146
View File
@@ -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<HashMap<String, Box<RawValue>>> {
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<bool>,
pub concurrency_key: Option<String>,
pub allow_wildcards: Option<bool>,
pub trigger_kind: Option<JobTriggerKind>,
}
impl From<ListCompletedQuery> for ListQueueQuery {
@@ -1812,6 +1853,7 @@ impl From<ListCompletedQuery> 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<JobTriggerKind>,
) -> error::Result<(Uuid, Option<String>)> {
#[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<JobTriggerKind>,
) -> error::Result<Response> {
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<JobTriggerKind>,
) -> error::Result<(Uuid, Option<String>)> {
#[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<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
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<JobTriggerKind>,
) -> error::Result<(Uuid, Option<String>)> {
#[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<JobTriggerKind>,
) -> error::Result<(Uuid, Option<bool>)> {
#[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<String>,
username: &str,
) -> error::Result<(Box<RawValue>, 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<RawValue>, 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<String>,
username: &str,
) -> error::Result<Response> {
@@ -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<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
method: hyper::http::Method,
args: RawWebhookArgs,
) -> error::Result<Response> {
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<DB>,
@@ -5397,20 +5597,10 @@ pub async fn stream_job(
is_get: bool,
) -> error::Result<Response> {
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<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
args: RawWebhookArgs,
) -> error::Result<Response> {
#[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<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
args: RawWebhookArgs,
) -> error::Result<Response> {
#[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::<Uuid>()
.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::<Uuid>()
.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<String>,
pub worker: Option<String>,
pub allow_wildcards: Option<bool>,
pub trigger_kind: Option<JobTriggerKind>,
}
async fn list_completed_jobs(
@@ -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",
];
+25
View File
@@ -738,6 +738,31 @@ async fn create_resource(
let res_value = resource.value.unwrap_or_default();
let raw_json = sqlx::types::Json(res_value.as_ref());
if resource.path.starts_with("f/app_themes/") {
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
w_id,
authed.username,
)
.execute(&db)
.await?;
} else if resource.path.starts_with("f/app_custom/") {
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
w_id,
authed.username,
)
.execute(&db)
.await?;
} else if resource.path.starts_with("f/app_groups/") {
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
w_id,
authed.username,
)
.execute(&db)
.await?;
}
sqlx::query!(
"INSERT INTO resource
(workspace_id, path, value, description, resource_type, created_by, edited_at)
+1
View File
@@ -1022,6 +1022,7 @@ async fn create_script_internal<'c>(
false,
None,
None,
None,
)
.await?;
Ok((hash, new_tx, None))
+39 -13
View File
@@ -188,7 +188,7 @@ pub async fn test_license_key(
Json(TestKey { license_key }): Json<TestKey>,
) -> error::Result<String> {
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| {
@@ -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()),
@@ -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?;
@@ -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<i64> {
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<HashMap<String, serde_json::Value>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Option<bool>, Option<String>)> {
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<HashMap<String, serde_json::Value>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<axum::response::Response> {
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<HashMap<String, serde_json::Value>>>,
trigger_path: String,
trigger_kind: Option<JobTriggerKind>,
) -> Result<axum::response::Response> {
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<HashMap<String, serde_json::Value>>>,
trigger_path: String,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Box<RawValue>, 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<HashMap<String, serde_json::Value>>>,
trigger_path: String,
trigger_kind: Option<JobTriggerKind>,
) -> Result<Box<RawValue>> {
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<HashMap<String, Box<RawValue>>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Option<bool>)> {
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<HashMap<String, Box<RawValue>>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Option<bool>)> {
#[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?;
@@ -94,6 +94,7 @@ impl ListeningTrigger<WebsocketConfig> {
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?;
}
@@ -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<RawValue>>,
) -> Result<HashMap<String, Box<RawValue>>> {
let args = if let Some(args) = args {
let args_map: Option<HashMap<String, serde_json::Value>> =
serde_json::from_str(args.get())
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?;
let args_map: Option<HashMap<String, serde_json::Value>> = 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?;
-32
View File
@@ -2455,38 +2455,6 @@ async fn create_workspace(
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
nw.id,
username,
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_custom', 'App Custom Components', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
nw.id,
username,
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms, created_by, edited_at) VALUES ($1, 'app_groups', 'App Groups', ARRAY[]::TEXT[], '{\"g/all\": false}', $2, now()) ON CONFLICT DO NOTHING",
nw.id,
username,
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO resource (workspace_id, path, value, description, resource_type, created_by, edited_at) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme', $2, now()) ON CONFLICT DO NOTHING",
nw.id,
username,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed,
+1
View File
@@ -66,6 +66,7 @@ pub struct FlowWithStarred {
pub starred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_error_logs: Option<String>,
pub version_id: i64,
}
fn is_none_or_false(b: &Option<bool>) -> bool {
+32 -17
View File
@@ -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>>(
+23 -2
View File
@@ -396,6 +396,21 @@ pub struct S3Resource {
pub port: Option<u16>,
}
impl S3Resource {
pub fn endpoint_with_region_fallback(&self, region_fallback: Option<String>) -> String {
if self.endpoint.is_empty() {
let final_region = if self.region.is_empty() {
region_fallback.unwrap_or_else(|| "us-east-1".to_string())
} else {
self.region.clone()
};
format!("s3.{}.amazonaws.com", final_region)
} else {
self.endpoint.clone()
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct AzureBlobResource {
pub endpoint: Option<String>,
@@ -642,7 +657,7 @@ pub async fn build_s3_client(s3_resource_ref: &S3Resource) -> error::Result<Arc<
let s3_resource = s3_resource_ref.clone();
let endpoint = render_endpoint(
s3_resource.endpoint,
s3_resource.endpoint_with_region_fallback(None),
s3_resource.use_ssl,
s3_resource.port,
s3_resource.path_style,
@@ -1216,7 +1231,13 @@ pub fn duckdb_connection_settings_internal(
duckdb_settings.push_str("SET s3_url_style='path';\n");
}
duckdb_settings.push_str(format!("SET s3_region='{}';\n", s3_resource.region).as_str());
duckdb_settings.push_str(format!("SET s3_endpoint='{}';\n", s3_resource.endpoint).as_str());
duckdb_settings.push_str(
format!(
"SET s3_endpoint='{}';\n",
s3_resource.endpoint_with_region_fallback(None)
)
.as_str(),
);
if !s3_resource.use_ssl {
duckdb_settings.push_str("SET s3_use_ssl=0;\n"); // default is true for DuckDB
}
+4 -4
View File
@@ -415,8 +415,8 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "duckdb"
version = "1.4.1"
source = "git+https://github.com/diegoimbert/duckdb-rs?branch=main#0df52a6941c9996d7ec60d585eaf6430db8c48cf"
version = "1.4.2"
source = "git+https://github.com/windmill-labs/duckdb-rs.git?rev=fe0702529de6ec5a568337726bba9355503157d2#fe0702529de6ec5a568337726bba9355503157d2"
dependencies = [
"arrow",
"cast",
@@ -702,8 +702,8 @@ checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libduckdb-sys"
version = "1.4.1"
source = "git+https://github.com/diegoimbert/duckdb-rs?branch=main#0df52a6941c9996d7ec60d585eaf6430db8c48cf"
version = "1.4.2"
source = "git+https://github.com/windmill-labs/duckdb-rs.git?rev=fe0702529de6ec5a568337726bba9355503157d2#fe0702529de6ec5a568337726bba9355503157d2"
dependencies = [
"cc",
"flate2",
@@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
chrono = "0.4.41"
duckdb = { git = "https://github.com/diegoimbert/duckdb-rs", branch = "main", features = ["bundled"] }
duckdb = { rev = "fe0702529de6ec5a568337726bba9355503157d2", git = "https://github.com/windmill-labs/duckdb-rs.git", features = ["bundled"] }
rust_decimal = "1.37.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
@@ -1,2 +1,2 @@
cargo build --release -p windmill_duckdb_ffi_internal
CARGO_NET_GIT_FETCH_WITH_CLI=1 cargo build --release -p windmill_duckdb_ffi_internal
cp target/release/libwindmill_duckdb_ffi_internal.* ../target/debug/
@@ -265,13 +265,22 @@ fn do_duckdb_inner(
None => {
type_aliases = Some(
(0..stmt.column_count())
.map(|i| stmt.column_logical_type(i).get_alias())
.map(|i| {
let logical_type = stmt.column_logical_type(i);
if logical_type.is_invalid() {
None
} else {
logical_type.get_alias()
}
})
.collect::<Vec<_>>(),
);
type_aliases.as_ref().unwrap()
}
};
// let type_aliases = (0..stmt.column_count()).map(|_| None).collect::<Vec<_>>();
let row = row_to_value(row, &column_names.as_slice(), &type_aliases.as_slice())
.map_err(|e| e.to_string())?;
rows_vec.push(row);
+12 -5
View File
@@ -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<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
#[cfg(feature = "cloud")]
if *CLOUD_HOSTED {
@@ -5278,11 +5283,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)))
+5 -7
View File
@@ -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?;
+1
View File
@@ -471,6 +471,7 @@ async fn execute_windmill_tool(
true,
None,
None,
None,
)
.await?;
+143 -3
View File
@@ -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<HashMap<String, Box<RawValue>>>>,
@@ -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);
}
}
+33 -11
View File
@@ -255,20 +255,42 @@ pub async fn handle_powershell_job(
job.args.as_ref()
};
let args_owned = windmill_parser_bash::parse_powershell_sig(&content)?
let parsed_sig = windmill_parser_bash::parse_powershell_sig(&content)?;
parsed_sig
.args
.iter()
.map(|arg| {
(
arg.name.clone(),
job_args.and_then(|x| x.get(&arg.name).map(|x| raw_to_pwsh_param(x.get()))),
)
})
.collect::<Vec<(String, Option<String>)>>();
.filter_map(|arg| {
let value_opt = job_args.and_then(|x| x.get(&arg.name));
args_owned
.into_iter()
.filter_map(|(n, v)| v.map(|v| format!("-{n} {v}")))
// Check if this is a switch parameter (only [switch], not [bool])
let is_switch = arg.otyp.as_ref().map(|t| {
t.to_lowercase() == "switch"
}).unwrap_or(false);
if is_switch {
// Handle switch parameters: -SwitchName or omit
if let Some(value) = value_opt {
match serde_json::from_str::<serde_json::Value>(value.get()) {
Ok(serde_json::Value::Bool(true)) => {
// Switch is enabled: just pass -SwitchName
Some(format!("-{}", arg.name))
}
Ok(serde_json::Value::Bool(false)) | _ => {
// Switch is disabled or invalid: omit the parameter
None
}
}
} else {
// No value provided, omit the switch (defaults to false)
None
}
} else {
// Regular parameter (including [bool]): format as -ParamName Value
// For [bool] parameters, this will be -ParamName $true or -ParamName $false
value_opt.map(|v| format!("-{} {}", arg.name, raw_to_pwsh_param(v.get())))
}
})
.collect::<Vec<_>>()
.join(" ")
};
@@ -3303,6 +3303,7 @@ async fn push_next_flow_job(
false,
None,
None,
None,
)
.warn_after_seconds(2)
.await?;
@@ -670,6 +670,7 @@ pub async fn trigger_dependents_to_recompute_dependencies(
false,
None,
debounce_job_id_o,
None,
)
.await?;
+1 -1
View File
@@ -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.576.3";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e
if [ -z "$1" ]; then
name="wmill"
else
name="$1"
fi
./gen_wm_client.sh
./windmill-utils-internal/gen_wm_client.sh
echo "Installing dev cli as $name (pass arg to override)"
deno install -f -A -g src/main.ts --name $name --unstable
+1 -1
View File
@@ -68,7 +68,7 @@ export {
// }
// });
export const VERSION = "1.575.0";
export const VERSION = "1.576.3";
export const WM_FORK_PREFIX = "wm-fork";
+8 -8
View File
@@ -1,14 +1,14 @@
/* tslint:disable */
/* eslint-disable */
export function parse_assets_sql(code: string): string;
export function parse_db_resource(code: string): string | undefined;
export function parse_bash(code: string): string;
export function parse_mssql(code: string): string;
export function parse_oracledb(code: string): string;
export function parse_powershell(code: string): string;
export function parse_bigquery(code: string): string;
export function parse_oracledb(code: string): string;
export function parse_graphql(code: string): string;
export function parse_snowflake(code: string): string;
export function parse_duckdb(code: string): string;
export function parse_sql(code: string): string;
export function parse_assets_sql(code: string): string;
export function parse_mysql(code: string): string;
export function parse_bash(code: string): string;
export function parse_duckdb(code: string): string;
export function parse_db_resource(code: string): string | undefined;
export function parse_bigquery(code: string): string;
export function parse_snowflake(code: string): string;
export function parse_sql(code: string): string;
+81 -81
View File
@@ -68,60 +68,6 @@ function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return decodeText(ptr, len);
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_assets_sql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_assets_sql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string | undefined}
*/
export function parse_db_resource(code) {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_db_resource(ptr0, len0);
let v2;
if (ret[0] !== 0) {
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
}
return v2;
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_bash(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_bash(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
@@ -141,25 +87,6 @@ export function parse_mssql(code) {
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_oracledb(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_oracledb(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
@@ -183,13 +110,13 @@ export function parse_powershell(code) {
* @param {string} code
* @returns {string}
*/
export function parse_bigquery(code) {
export function parse_oracledb(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_bigquery(ptr0, len0);
const ret = wasm.parse_oracledb(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -221,13 +148,51 @@ export function parse_graphql(code) {
* @param {string} code
* @returns {string}
*/
export function parse_snowflake(code) {
export function parse_assets_sql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_snowflake(ptr0, len0);
const ret = wasm.parse_assets_sql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_mysql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_mysql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_bash(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_bash(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -255,17 +220,33 @@ export function parse_duckdb(code) {
}
}
/**
* @param {string} code
* @returns {string | undefined}
*/
export function parse_db_resource(code) {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_db_resource(ptr0, len0);
let v2;
if (ret[0] !== 0) {
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
}
return v2;
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_sql(code) {
export function parse_bigquery(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_sql(ptr0, len0);
const ret = wasm.parse_bigquery(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -278,13 +259,32 @@ export function parse_sql(code) {
* @param {string} code
* @returns {string}
*/
export function parse_mysql(code) {
export function parse_snowflake(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_mysql(ptr0, len0);
const ret = wasm.parse_snowflake(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* @param {string} code
* @returns {string}
*/
export function parse_sql(code) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(code, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.parse_sql(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
Binary file not shown.
+10 -33
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.575.0",
"version": "1.576.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.575.0",
"version": "1.576.3",
"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",
@@ -79,7 +79,7 @@
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.574.1",
"windmill-parser-wasm-py": "1.538.0",
"windmill-parser-wasm-regex": "1.574.1",
"windmill-parser-wasm-regex": "1.575.4",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.558.1",
"windmill-parser-wasm-ts": "1.565.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"
@@ -13759,9 +13759,9 @@
"integrity": "sha512-s+bdIgT/fA5em3zYUwF8D14uA/dZh7iu0krZYZQqZUO7txN37hwSCVfovbMkIwm4zPbsJ50mU8DRLt7UpAPZIw=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.574.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.574.1.tgz",
"integrity": "sha512-KnNBnpTGcnBPSzTQHdsjTPJbRQ81iem98eUnMGO8Zt1qgBFJ/eFZPwmUg6TZMbMITzTi0QqRCJj08eQID1FcPg=="
"version": "1.575.4",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.575.4.tgz",
"integrity": "sha512-G7Ni8XxdSKLNew50B1KRmwn/z9qq3obpEDaaDFS7Z74MeUdaBB/TD69SY6RxDL8SZrh0Tl7SCbS/GGLnt1AzkQ=="
},
"node_modules/windmill-parser-wasm-ruby": {
"version": "1.526.1",
@@ -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",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.575.0",
"version": "1.576.3",
"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",
@@ -144,7 +144,7 @@
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.574.1",
"windmill-parser-wasm-py": "1.538.0",
"windmill-parser-wasm-regex": "1.574.1",
"windmill-parser-wasm-regex": "1.575.4",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.558.1",
"windmill-parser-wasm-ts": "1.565.0",
@@ -0,0 +1,84 @@
import { CancelablePromise } from './gen'
export namespace CancelablePromiseUtils {
export function then<T, U>(
promise: CancelablePromise<T>,
f: (value: T) => CancelablePromise<U>
): CancelablePromise<U> {
let promiseToBeCanceled: CancelablePromise<any> = promise
let p = new CancelablePromise<U>((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<T>(value: T): CancelablePromise<T> {
return new CancelablePromise((resolve) => resolve(value))
}
export function err<T>(error: any): CancelablePromise<T> {
return new CancelablePromise((_, reject) => reject(error))
}
export function map<T, U>(
promise: CancelablePromise<T>,
f: (value: T) => U
): CancelablePromise<U> {
return then(promise, (value) => pure(f(value)))
}
export function pipe<T>(
promise: CancelablePromise<T>,
f: (value: T) => void
): CancelablePromise<T> {
promise.then((value) => {
f(value)
})
return promise
}
export function catchErr<T, U>(
promise: CancelablePromise<T>,
f: (error: any) => CancelablePromise<U>
): CancelablePromise<T | U> {
let promiseToBeCanceled: CancelablePromise<any> = promise
let p = new CancelablePromise<T | U>((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<T>(promise: CancelablePromise<T>, f: () => void): CancelablePromise<T> {
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<T>(
promise: CancelablePromise<T>,
timeoutMs: number,
onTimeout: () => void
): CancelablePromise<T> {
let timeoutId: number | undefined = setTimeout(onTimeout, timeoutMs)
promise = finallyDo(promise, () => {
if (timeoutId !== undefined) clearTimeout(timeoutId)
})
return promise
}
}
@@ -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
}
@@ -42,7 +42,7 @@
<Button variant="default" on:click={appConnect?.back ?? (() => {})}>Back</Button>
{/if}
<Button {disabled} on:click={appConnect?.next ?? (() => {})}>
<Button variant="accent" {disabled} on:click={appConnect?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
+7 -3
View File
@@ -19,6 +19,7 @@
import { onDestroy, onMount } from 'svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Button from './common/button/Button.svelte'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
rd?: string | undefined
@@ -212,8 +213,9 @@
function popupListener(event) {
let data = event.data
console.log('popupListener', data, event.origin, window.location.origin)
if (event.origin !== window.location.origin) {
// console.log('popupListener', data, event.origin, window.location.origin)
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log('popupListener from different origin', event.origin, window.location.origin)
return
}
@@ -261,7 +263,9 @@
console.error('Could not persist redirection to local storage', e)
}
}
let url = base + '/api/oauth/login/' + provider
let url = base + '/api/oauth/login/' + provider + (popup ? '?close=true' : '')
console.log('storeRedirect', popup, url)
if (popup) {
localStorage.setItem('closeUponLogin', 'true')
window.addEventListener('message', popupListener)
+10 -7
View File
@@ -28,6 +28,7 @@
selectedIds?: string[]
canSelect?: boolean
lastFetchWentToEnd?: boolean
totalRowsFetched: number
onPointClicked: (ids: string[]) => void
onLoadExtra: () => void
onZoom: (zoom: { min: Date; max: Date }) => void
@@ -41,6 +42,7 @@
selectedIds = $bindable([]),
canSelect = true,
lastFetchWentToEnd = false,
totalRowsFetched,
onPointClicked,
onLoadExtra,
onZoom
@@ -301,13 +303,14 @@
<!-- {JSON.stringify(jobs?.map((x) => x.started_at))} -->
<div class="relative max-h-40">
{#if !lastFetchWentToEnd}
<div class="absolute top-[-28px] left-[220px]"
><Button size="xs" color="transparent" variant="contained" on:click={() => onLoadExtra()}
>Load more <Tooltip2
>There are more jobs to load but only the first 1000 were fetched</Tooltip2
></Button
></div
>
<div class="absolute top-[-28px] left-[220px]">
<Button size="xs" color="transparent" variant="contained" on:click={() => onLoadExtra()}>
Load more
<Tooltip2>
There are more jobs to load but only the first {totalRowsFetched} were fetched
</Tooltip2>
</Button>
</div>
{/if}
<Scatter {data} options={scatterOptions} />
</div>
File diff suppressed because it is too large Load Diff
@@ -1668,7 +1668,7 @@
args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args}
isDeployed={savedScript && !savedScript?.draft_only}
schema={script.schema}
hash={script.parent_hash}
runnableVersion={script.parent_hash}
onDeployTrigger={handleDeployTrigger}
/>
@@ -0,0 +1,145 @@
<script lang="ts">
import { User, UserRoundX } from 'lucide-svelte'
import { enterpriseLicense, userStore } from '$lib/stores'
import { base } from '$app/paths'
import { page } from '$app/state'
import Login from '$lib/components/Login.svelte'
import { isCloudHosted } from '$lib/cloud'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { onMount, setContext } from 'svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '../types'
import { UserService, type AppWithLastVersion, type GlobalWhoamiResponse } from '$lib/gen'
import { urlParamsToObject } from '$lib/utils'
import { goto } from '$app/navigation'
import AppPreview from './AppPreview.svelte'
import { twMerge } from 'tailwind-merge'
import { writable } from 'svelte/store'
let {
notExists,
noPermission,
jwtError,
onLoginSuccess,
app,
workspace
}: {
notExists: boolean
noPermission: boolean
jwtError: boolean
onLoginSuccess: () => void
app: (AppWithLastVersion & { value: any }) | undefined
workspace: string | undefined
} = $props()
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
const breakpoint = writable<EditorBreakpoint>('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')
}
let globalUser = $state<GlobalWhoamiResponse | undefined>(undefined)
async function loadGlobalUser() {
try {
globalUser = await UserService.globalWhoami()
} catch (error) {
console.error(error)
}
// const user = await fetch('/api/global/user')
// console.log(user)
}
onMount(() => {
// this is to avoid loading global user if the userStore is set at loading
setTimeout(() => {
if ($userStore) return
loadGlobalUser()
}, 2000)
})
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
{#snippet userInfo(child)}
<div class="flex gap-1 items-center"><User size={14} />{child}</div>
{/snippet}
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
{@render userInfo($userStore.username)}
{:else if globalUser}
{@render userInfo(globalUser.email)}
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"> This app requires read access </div>
<div class="text-center mt-8 text-sm text-primary">
{#if $userStore}You are logged in but have no read access to this app{:else if globalUser && workspace}
You are logged in but are not a member of the workspace <span class="text-xl font-bold"
>{workspace}</span
> this app is part of
{:else}You must be logged in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login {onLoginSuccess} popup rd={page.url.toString()} />
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
{workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
@@ -26,22 +26,23 @@
UserService,
ScriptService,
FlowService,
AppService
AppService,
CancelError
} from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { ChevronDown, Loader2, RefreshCcw } from 'lucide-svelte'
import { onDestroy, tick, untrack } from 'svelte'
import { onDestroy, untrack } from 'svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import Select from '../select/Select.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { safeSelectItems } from '../select/utils.svelte'
import { CancelablePromiseUtils } from '$lib/cancelable-promise-utils'
import { sendUserToast } from '$lib/toast'
let usernames: string[] | undefined = $state()
let resources = usePromise(() => loadResources($workspaceStore!), { loadInit: false })
let loading: boolean = $state(false)
let page: number | undefined = undefined
interface Props {
logs?: AuditLog[]
@@ -55,6 +56,7 @@
resource?: string | undefined
actionKind?: ActionKind | 'all'
scope?: undefined | 'all_workspaces' | 'instance'
loading?: boolean
}
let {
@@ -68,7 +70,8 @@
operation = $bindable(),
resource = $bindable() as string | undefined,
actionKind = $bindable(undefined),
scope = $bindable(undefined)
scope = $bindable(undefined),
loading = $bindable(false)
}: Props = $props()
$effect.pre(() => {
@@ -86,50 +89,45 @@
}
})
async function loadLogs(
username: string | undefined,
page: number | undefined,
perPage: number | undefined,
before: string | undefined,
after: string | undefined,
operation: string | undefined,
resource: string | undefined,
actionKind: ActionKind | undefined | 'all',
scope: undefined | 'all_workspaces' | 'instance'
): Promise<void> {
function loadLogs() {
loading = true
if (username == 'all') {
username = undefined
}
if (operation == 'all' || operation == '') {
operation = undefined
}
let username_ = username == 'all' ? undefined : username
let operation_ = operation == 'all' || operation == '' ? undefined : operation
let actionKind_ = actionKind == 'all' ? undefined : actionKind
let resource_ = resource == 'all' || resource == '' ? undefined : resource
// @ts-ignore
if (actionKind == 'all' || actionKind == '') {
actionKind = undefined
}
if (resource == 'all' || resource == '') {
resource = undefined
}
logs = await AuditService.listAuditLogs({
let _promise = AuditService.listAuditLogs({
workspace: scope === 'instance' ? 'global' : $workspaceStore!,
page,
page: pageIndex,
perPage,
before,
after,
username,
operation,
resource,
actionKind,
username: username_,
operation: operation_,
resource: resource_,
actionKind: actionKind_,
allWorkspaces: scope === 'all_workspaces'
})
hasMore = logs.length > 0 && logs.length === perPage
loading = false
let promise = CancelablePromiseUtils.map(_promise, (value) => {
logs = value
hasMore = !logs || (logs.length > 0 && logs.length === perPage)
loading = false
})
promise = CancelablePromiseUtils.onTimeout(promise, 4000, () => {
sendUserToast(
'Loading audit logs is taking longer than expected...',
true,
perPage > 25
? [{ label: 'Reduce to 25 items per page', callback: () => (perPage = 25) }]
: []
)
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
if (e instanceof CancelError) return CancelablePromiseUtils.pure<void>(undefined)
return CancelablePromiseUtils.err<void>(e)
})
return promise
}
async function loadUsers() {
@@ -139,17 +137,7 @@
: [$userStore?.username ?? '']
}
let initialLoad = true
function refreshLogs() {
loadUsers()
resources.refresh()
loadLogs(username, page, perPage, before, after, operation, resource, actionKind, scope)
tick().then(() => {
initialLoad = false
})
}
function updateLogs() {
function updateQueryParams() {
const queryParams: string[] = []
function addQueryParam(key: string, value: string | number | undefined | null) {
@@ -159,7 +147,7 @@
}
addQueryParam('username', username)
addQueryParam('page', page)
addQueryParam('page', pageIndex)
addQueryParam('perPage', perPage)
addQueryParam('before', before)
addQueryParam('after', after)
@@ -172,25 +160,6 @@
}
const query = '?' + queryParams.join('&')
goto(query, { replaceState: true, keepFocus: true })
loadLogs(username, page, perPage, before, after, operation, resource, actionKind, scope)
}
function updateQueryParams() {
if (initialLoad) {
return
}
page = 1
pageIndex = 1
updateLogs()
}
function updatePageQueryParams(pageIndex?: number | undefined) {
if (initialLoad) {
return
}
page = pageIndex
updateLogs()
}
window.addEventListener('popstate', handlePopState)
@@ -307,18 +276,22 @@
WORKSPACES_DELETE: 'workspaces.delete'
}
let refresh = $state(1)
$effect(() => {
$workspaceStore && refresh && untrack(() => refreshLogs())
})
let refresh = $state(0)
let lastRefresh = $state(-1)
// observe all the variables that should trigger an update
$effect(() => {
;[username, perPage, before, after, operation, resource, actionKind, scope]
updateQueryParams()
})
// observe the pageIndex variable that should trigger an update
$effect(() => {
updatePageQueryParams(pageIndex)
;[refresh, username, perPage, before, after, operation, resource, actionKind, scope, pageIndex]
return untrack(() => {
if (refresh !== lastRefresh) {
loadUsers()
resources.refresh()
lastRefresh = refresh
}
updateQueryParams()
let promise = loadLogs()
return () => promise?.cancel()
})
})
</script>
@@ -390,25 +363,30 @@
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute font-semibold text-emphasis -top-4">Username</span>
<select bind:value={username}>
{#if usernames}
{#if $userStore?.is_admin || $userStore?.is_super_admin}
<option selected>all</option>
{/if}
{#each usernames as e}
{#if e == username || $userStore?.is_admin || $userStore?.is_super_admin}
<option>{e}</option>
{:else}
<option disabled>{e}</option>
{/if}
{/each}
{/if}
</select>
<Select
bind:value={username}
class="w-full"
disablePortal
RightIcon={ChevronDown}
items={usernames
? [
...($userStore?.is_admin || $userStore?.is_super_admin
? [{ value: 'all', label: 'all' }]
: []),
...usernames.map((e) => ({
value: e,
label: e,
disabled: e !== username && !$userStore?.is_admin && !$userStore?.is_super_admin
}))
]
: []}
/>
</div>
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute font-semibold text-emphasis -top-4">Resource</span>
<Select
disablePortal
onCreateItem={(r) => (resources.value?.push(r), (resource = r))}
createText="Press enter to use this value"
bind:value={resource}
@@ -423,6 +401,7 @@
<span class="text-xs absolute font-semibold text-emphasis -top-4">Operation</span>
<Select
disablePortal
bind:value={operation}
items={['all', ...Object.values(operations)].map((r) => ({ value: r, label: r }))}
inputClass="dark:!bg-gray-700"
@@ -434,12 +413,19 @@
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute font-semibold text-emphasis -top-4">Action</span>
<select class="!truncate" bind:value={actionKind}>
<option selected value="all">all</option>
{#each ['Create', 'Update', 'Delete', 'Execute'] as e}
<option value={e.toLocaleLowerCase()}>{e}</option>
{/each}
</select>
<Select
class="w-full"
bind:value={actionKind}
disablePortal
RightIcon={ChevronDown}
items={[
{ value: 'all', label: 'all' },
{ value: 'create', label: 'Create' },
{ value: 'update', label: 'Update' },
{ value: 'delete', label: 'Delete' },
{ value: 'execute', label: 'Execute' }
]}
/>
</div>
<div class="flex flex-row gap-1">
@@ -4,7 +4,7 @@
import { displayDate } from '$lib/utils'
import { onMount, tick } from 'svelte'
import Button from '../common/button/Button.svelte'
import { ChevronLeft, ChevronRight, ListFilterPlus } from 'lucide-svelte'
import { ChevronLeft, ChevronRight, ListFilterPlus, Loader2 } from 'lucide-svelte'
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
import { twMerge } from 'tailwind-merge'
@@ -19,6 +19,7 @@
usernameFilter?: string | undefined
resourceFilter?: string | undefined
showWorkspace?: boolean
loading?: boolean
onselect?: (id: number) => void
}
@@ -33,7 +34,8 @@
usernameFilter = $bindable(),
resourceFilter = $bindable(),
showWorkspace = false,
onselect
onselect,
loading
}: Props = $props()
function groupLogsByDay(logs: AuditLog[]): Record<string, AuditLog[]> {
@@ -120,6 +122,7 @@
}
return 'gray'
}
let height = $derived(tableHeight - headerHeight - footerHeight)
</script>
<svelte:window onresize={() => computeHeight()} />
@@ -139,12 +142,22 @@
<div class="w-2/12">Resource</div>
</div>
</div>
{#if logs?.length == 0}
<div class="text-xs text-secondary p-8"> No logs found for the selected filters. </div>
{#if loading}
<div style="height: {height}px;" class="flex justify-center items-center">
<Loader2 class="animate-spin" />
</div>
{:else if !logs?.length}
<div
class="text-xs text-secondary p-8 flex justify-center items-center"
style="height: {height}px;"
>
No logs found for the selected filters.
</div>
{:else}
<VirtualList
width="100%"
height={tableHeight - headerHeight - footerHeight}
{height}
itemCount={flatLogs?.length ?? 0}
itemSize={(index) => {
if (flatLogs?.[index]?.type === 'date') {
@@ -550,7 +550,7 @@
)
</script>
<div class="p-4 bg-surface mb-4 h-full">
<div class="p-4 bg-surface mb-4 h-full text-xs">
{#if logs.length === 0}
<div class="text-center py-8 text-secondary"> No audit logs to display </div>
{:else if !groupedData || groupedData.status === 'loading'}
+163 -137
View File
@@ -6,7 +6,9 @@
type CompletedJob,
type ExtendedJobs,
ConcurrencyGroupsService,
type ObscuredJob
type ObscuredJob,
CancelablePromise,
CancelError
} from '$lib/gen'
import { sendUserToast } from '$lib/toast'
@@ -14,6 +16,8 @@
import { tweened, type Tweened } from 'svelte/motion'
import { subtractDaysFromDateString } from '$lib/utils'
import type { JobTriggerType } from '../triggers/utils'
import { CancelablePromiseUtils } from '$lib/cancelable-promise-utils'
interface Props {
jobs: Job[] | undefined
@@ -27,6 +31,7 @@
showFutureJobs?: boolean
argFilter: string | undefined
resultFilter?: string | undefined
jobTriggerKind?: JobTriggerType | undefined
schedulePath?: string | undefined
jobKindsCat?: string | undefined
minTs?: string | undefined
@@ -49,7 +54,7 @@
allWorkspaces?: boolean
computeMinAndMax: (() => { minTs: string; maxTs: string | undefined } | undefined) | undefined
lookback?: number
perPage?: number | undefined
perPage?: number
allowWildcards?: boolean
}
@@ -66,6 +71,7 @@
showFutureJobs = true,
argFilter,
resultFilter = undefined,
jobTriggerKind = undefined,
schedulePath = undefined,
jobKindsCat = undefined,
minTs = $bindable(undefined),
@@ -87,7 +93,7 @@
allWorkspaces = false,
computeMinAndMax,
lookback = 0,
perPage = undefined,
perPage = $bindable(1000),
allowWildcards = false
}: Props = $props()
let intervalId: number | undefined = $state()
@@ -95,7 +101,23 @@
function onParamChanges() {
resetJobs()
loadJobsIntern(true)
let promise = loadJobsIntern(true)
promise = CancelablePromiseUtils.onTimeout(promise, 4000, () => {
sendUserToast(
'Loading jobs is taking longer than expected...',
true,
perPage > 25
? [{ label: 'Reduce to 25 items per page', callback: () => (perPage = 25) }]
: []
)
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
if (e instanceof CancelError) {
return CancelablePromiseUtils.pure<void>(undefined)
}
return CancelablePromiseUtils.err(e)
})
return promise
}
function computeJobKinds(jobKindsCat: string | undefined): string {
@@ -162,123 +184,121 @@
const minCreated = new Date(new Date(ts).getTime() - 1).toISOString()
let olderJobs = await fetchJobs(minCreated, minTs, undefined)
jobs = jobs.concat(olderJobs)
jobs = jobs?.concat(olderJobs)
computeCompletedJobs()
return olderJobs?.length < 1000
return olderJobs?.length < perPage
}
return false
}
async function fetchJobs(
function fetchJobs(
completedBefore: string | undefined,
completedAfter: string | undefined,
createdAfterQueue: string | undefined
): Promise<Job[]> {
): CancelablePromise<Job[]> {
loadingFetch = true
try {
let scriptPathStart = folder === null || folder === '' ? undefined : `f/${folder}/`
let scriptPathExact = path === null || path === '' ? undefined : path
return JobService.listJobs({
workspace: $workspaceStore!,
completedBefore,
completedAfter,
createdAfterQueue,
schedulePath,
scriptPathExact,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: scriptPathStart,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running:
success == 'running' || success == 'suspended'
? true
: success == 'waiting'
? false
: undefined,
isSkipped: showSkipped ? undefined : false,
// isFlowStep: jobKindsCat != 'all' ? false : undefined,
hasNullParent: jobKindsCat != 'all' ? true : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
worker: worker === null || worker === '' ? undefined : worker,
isNotSchedule: showSchedules == false ? true : undefined,
suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined,
scheduledForBeforeNow:
showFutureJobs == false || success == 'waiting' || success == 'suspended'
? true
let scriptPathStart = folder === null || folder === '' ? undefined : `f/${folder}/`
let scriptPathExact = path === null || path === '' ? undefined : path
let promise = JobService.listJobs({
workspace: $workspaceStore!,
completedBefore,
completedAfter,
createdAfterQueue,
schedulePath,
scriptPathExact,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: scriptPathStart,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running:
success == 'running' || success == 'suspended'
? true
: success == 'waiting'
? false
: undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
? argFilter
: undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards: allowWildcards ? true : undefined
})
} catch (e) {
isSkipped: showSkipped ? undefined : false,
// isFlowStep: jobKindsCat != 'all' ? false : undefined,
hasNullParent: jobKindsCat != 'all' ? true : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
worker: worker === null || worker === '' ? undefined : worker,
isNotSchedule: showSchedules == false ? true : undefined,
suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined,
scheduledForBeforeNow:
showFutureJobs == false || success == 'waiting' || success == 'suspended'
? true
: undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
triggerKind: jobTriggerKind,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards: allowWildcards ? true : undefined
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
if (e instanceof CancelError) return CancelablePromiseUtils.err(e)
sendUserToast('There was an issue loading jobs, see browser console for more details', true)
console.error(e)
return []
} finally {
return CancelablePromiseUtils.pure([] as Job[])
})
CancelablePromiseUtils.pipe(promise, () => {
loadingFetch = false
}
})
return promise
}
async function fetchExtendedJobs(
function fetchExtendedJobs(
concurrencyKey: string | null,
createdBeforeQueue: string | undefined,
completedAfter: string | undefined
): Promise<ExtendedJobs> {
): CancelablePromise<ExtendedJobs> {
loadingFetch = true
try {
return ConcurrencyGroupsService.listExtendedJobs({
rowLimit: 1000,
concurrencyKey: concurrencyKey == null || concurrencyKey == '' ? undefined : concurrencyKey,
workspace: $workspaceStore!,
completedAfter,
createdBeforeQueue: createdBeforeQueue,
// createdOrStartedBefore: startedBefore,
// createdOrStartedAfter: startedAfter,
// createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
schedulePath,
scriptPathExact: path === null || path === '' ? undefined : path,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running: success == 'running' ? true : undefined,
isSkipped: showSkipped ? undefined : false,
isFlowStep: jobKindsCat != 'all' ? false : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
isNotSchedule: showSchedules == false ? true : undefined,
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
? argFilter
: undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards
})
} catch (e) {
let promise = ConcurrencyGroupsService.listExtendedJobs({
rowLimit: perPage,
concurrencyKey: concurrencyKey == null || concurrencyKey == '' ? undefined : concurrencyKey,
workspace: $workspaceStore!,
completedAfter,
createdBeforeQueue: createdBeforeQueue,
// createdOrStartedBefore: startedBefore,
// createdOrStartedAfter: startedAfter,
// createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
schedulePath,
scriptPathExact: path === null || path === '' ? undefined : path,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running: success == 'running' ? true : undefined,
isSkipped: showSkipped ? undefined : false,
isFlowStep: jobKindsCat != 'all' ? false : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
isNotSchedule: showSchedules == false ? true : undefined,
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
triggerKind: jobTriggerKind,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
sendUserToast('There was an issue loading jobs, see browser console for more details', true)
console.error(e)
return {
jobs: [],
obscured_jobs: []
}
} finally {
return CancelablePromiseUtils.pure({ jobs: [], obscured_jobs: [] } as ExtendedJobs)
})
promise = CancelablePromiseUtils.pipe(promise, () => {
loadingFetch = false
}
})
return promise
}
export async function loadJobs(
@@ -303,54 +323,58 @@
intervalId && clearInterval(intervalId)
intervalId = setInterval(syncer, refreshRate)
}
async function loadJobsIntern(shouldGetCount?: boolean): Promise<void> {
function loadJobsIntern(shouldGetCount?: boolean): CancelablePromise<void> {
if (shouldGetCount) {
getCount()
}
loading = true
try {
// Extend MinTs to fetch jobs mefore minTs and show a correct concurrency graph
// TODO: when an ended_at column is created on the completed_job table,
// lookback won't be needed anymore (just filter ended_at > minTs instead
const extendedMinTs = subtractDaysFromDateString(minTs, lookback)
if (concurrencyKey == null || concurrencyKey === '') {
let newJobs = await fetchJobs(maxTs, undefined, extendedMinTs)
// Extend MinTs to fetch jobs mefore minTs and show a correct concurrency graph
// TODO: when an ended_at column is created on the completed_job table,
// lookback won't be needed anymore (just filter ended_at > minTs instead
const extendedMinTs = subtractDaysFromDateString(minTs, lookback)
if (concurrencyKey == null || concurrencyKey === '') {
return CancelablePromiseUtils.map(fetchJobs(maxTs, undefined, extendedMinTs), (newJobs) => {
extendedJobs = { jobs: newJobs, obscured_jobs: [] } as ExtendedJobs
// Filter on minTs here and not in the backend
// to get enough data for the concurrency graph
jobs = sortMinDate(minTs, newJobs)
externalJobs = []
} else {
extendedJobs = await fetchExtendedJobs(concurrencyKey, maxTs, extendedMinTs)
const newJobs = extendedJobs.jobs
const newExternalJobs = extendedJobs.obscured_jobs
computeCompletedJobs()
loading = false
})
} else {
return CancelablePromiseUtils.map(
fetchExtendedJobs(concurrencyKey, maxTs, extendedMinTs),
(newExtendedJobs) => {
extendedJobs = newExtendedJobs
const newJobs = newExtendedJobs.jobs
const newExternalJobs = newExtendedJobs.obscured_jobs
// Filter on minTs here and not in the backend
// to get enough data for the concurrency graph
if (minTs != undefined) {
const minDate = new Date(minTs)
jobs = newJobs.filter((x) =>
x.started_at
? new Date(x.started_at) > minDate
: x.created_at
? new Date(x.created_at) > minDate
: false
)
externalJobs = computeExternalJobs(
newExternalJobs.filter((x) => x.started_at && new Date(x.started_at) > minDate)
)
} else {
jobs = newJobs
externalJobs = computeExternalJobs(newExternalJobs)
// Filter on minTs here and not in the backend
// to get enough data for the concurrency graph
if (minTs != undefined) {
const minDate = new Date(minTs)
jobs = newJobs.filter((x) =>
x.started_at
? new Date(x.started_at) > minDate
: x.created_at
? new Date(x.created_at) > minDate
: false
)
externalJobs = computeExternalJobs(
newExternalJobs.filter((x) => x.started_at && new Date(x.started_at) > minDate)
)
} else {
jobs = newJobs
externalJobs = computeExternalJobs(newExternalJobs)
}
computeCompletedJobs()
loading = false
}
}
computeCompletedJobs()
} catch (err) {
sendUserToast(`There was a problem fetching jobs: ${err}`, true)
console.error(JSON.stringify(err))
)
}
loading = false
}
async function getCount() {
@@ -557,10 +581,12 @@
showSchedules,
allWorkspaces,
argFilter,
resultFilter
resultFilter,
jobTriggerKind,
perPage
]
untrack(() => onParamChanges())
let p = untrack(() => onParamChanges())
return () => p.cancel()
})
$effect(() => {
;[autoRefresh, refreshRate]
@@ -4,12 +4,13 @@
import { createEventDispatcher } from 'svelte'
interface Props {
minTs: string | undefined;
maxTs: string | undefined;
loading?: boolean;
selectedManualDate?: number;
loadText?: string | undefined;
serviceLogsChoices?: boolean;
minTs: string | undefined
maxTs: string | undefined
loading?: boolean
selectedManualDate?: number
loadText?: string | undefined
serviceLogsChoices?: boolean
numberOfLastJobsToFetch?: number
}
let {
@@ -18,8 +19,9 @@
loading = false,
selectedManualDate = $bindable(0),
loadText = undefined,
serviceLogsChoices = false
}: Props = $props();
serviceLogsChoices = false,
numberOfLastJobsToFetch = 1000
}: Props = $props()
export function computeMinMax(): { minTs: string; maxTs: string | undefined } | undefined {
return manualDates[selectedManualDate].computeMinMax()
@@ -75,7 +77,7 @@
let manualDates = $derived([
{
label: loadText ?? 'Last 1000 runs',
label: loadText ?? `Last ${numberOfLastJobsToFetch} runs`,
computeMinMax: () => {
return undefined
}
@@ -2,7 +2,6 @@
import { Button } from '../common'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import Tooltip from '../Tooltip.svelte'
import { CircleAlert, CircleCheck, Hourglass, ListFilterPlus, CirclePlay, X } from 'lucide-svelte'
import JsonEditor from '../JsonEditor.svelte'
import Toggle from '../Toggle.svelte'
@@ -18,6 +17,7 @@
import DropdownSelect from '../DropdownSelect.svelte'
import TooltipV2 from '$lib/components/meltComponents/Tooltip.svelte'
import TextInput from '../text_input/TextInput.svelte'
import { jobTriggerTypes, triggerDisplayNamesMap, type JobTriggerType } from '../triggers/utils'
interface Props {
// Filters
@@ -31,6 +31,7 @@
argFilter: string
argError: string
resultFilter: string
jobTriggerKind: JobTriggerType | undefined
resultError: string
jobKindsCat: string
user?: string | null
@@ -67,6 +68,7 @@
argFilter = $bindable(),
argError = $bindable(),
resultFilter = $bindable(),
jobTriggerKind = $bindable(),
resultError = $bindable(),
jobKindsCat = $bindable(),
user = $bindable(null),
@@ -949,26 +951,40 @@
{/if}
<Label label="Show skipped flows">
<span class="text-2xs text-secondary">
Skipped flows are flows that did an early break
</span>
<div class="flex flex-row gap-1 items-center">
<Toggle size="sm" bind:checked={showSkipped} />
</div>
{#snippet header()}
<Tooltip>Skipped flows are flows that did an early break</Tooltip>
{/snippet}
</Label>
<Label label="Filter by trigger kind">
<span class="text-2xs text-secondary">
{`Filter by what kind of trigger started the run.`}
</span>
<Select
items={jobTriggerTypes.map((value) => ({
label: triggerDisplayNamesMap[value],
value
}))}
bind:value={jobTriggerKind}
clearable
/>
</Label>
<div class="flex flex-col gap-6">
<Label label="Filter by args">
<JsonEditor bind:error={argError} bind:code={copyArgFilter} />
<span class="text-2xs text-secondary">
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
</span>
<JsonEditor bind:error={argError} bind:code={copyArgFilter} />
</Label>
<Label label="Filter by result">
<JsonEditor bind:error={resultError} bind:code={copyResultFilter} />
<span class="text-2xs text-secondary">
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
</span>
<JsonEditor bind:error={resultError} bind:code={copyResultFilter} />
</Label>
</div>
@@ -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()}
<div
>{#if !lastFetchWentToEnd && jobs && jobs.length >= 1000}
>{#if !lastFetchWentToEnd && jobs && jobs.length >= perPage}
<button
class="text-xs text-blue-600 text-center w-full pb-2"
class="text-xs text-accent text-center w-full pb-2"
onclick={() => {
dispatch('loadExtra')
}}>Load next 1000 jobs</button
}}
>
Load next {perPage} jobs
</button>
{/if}</div
>
{/snippet}
@@ -1,4 +1,7 @@
<script lang="ts" generics="Item extends { label?: string; value: any; subtitle?: string }">
<script
lang="ts"
generics="Item extends { label?: string; value: any; subtitle?: string; disabled?: boolean }"
>
import { clickOutside } from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../common/CloseButton.svelte'
@@ -195,11 +195,12 @@
: item.value === value
? 'bg-surface-hover/60'
: 'hover:bg-surface-hover',
itemButtonWrapperClasses
itemButtonWrapperClasses,
item.disabled ? 'cursor-not-allowed text-disabled' : ''
)}
onclick={(e) => {
e.stopImmediatePropagation()
onSelectValue(item)
if (!item.disabled) onSelectValue(item)
}}
>
{@render startSnippet?.({ item, close: () => (open = false) })}
@@ -44,6 +44,7 @@ export type ProcessedItem<T> = {
label: string
value: T
subtitle?: string
disabled?: boolean
}
export function getLabel<T>(item: { label?: string; value: T } | undefined): string | undefined {
@@ -51,7 +52,7 @@ export function getLabel<T>(item: { label?: string; value: T } | undefined): str
if (item.label) return item.label
if (typeof item.value === 'string') return item.value
if (typeof item.value == 'number' || typeof item.value == 'boolean') return item.value.toString()
if (item.value == null) { return undefined }
if (item.value == null) return undefined
return JSON.stringify(item.value)
}
@@ -272,11 +272,7 @@
<div class="flex flex-col gap-1 mb-4 w-full">
<div class="flex justify-center w-full">
<div class="relative h-fit">
<AnimatedButton
animate={captureInfo.active}
wrapperClasses={captureInfo.active ? 'm-[-2px]' : ''}
baseRadius="7px"
>
<AnimatedButton animate={captureInfo.active} baseRadius="6px">
<Button
size="xs"
on:click={() => dispatch('captureToggle', {})}
@@ -287,6 +283,7 @@
? { icon: CircleStop }
: { icon: CaptureIcon, props: { variant: 'redDot' } }}
loading={captureLoading}
btnClasses={captureInfo.active ? '!bg-surface' : ''}
>
{#if captureInfo.active}
<p class="w-24" transition:fade={{ duration: 300 }}>Stop capturing</p>
@@ -41,7 +41,7 @@
newItem?: boolean
currentPath: string
fakeInitialPath?: string
hash?: string | undefined
runnableVersion?: string | undefined
args?: Record<string, any>
initialPath: string
isFlow: boolean
@@ -59,7 +59,7 @@
newItem = false,
currentPath,
fakeInitialPath = '',
hash = undefined,
runnableVersion = undefined,
args = {},
initialPath,
isFlow,
@@ -363,7 +363,7 @@
{initialPath}
{fakeInitialPath}
{currentPath}
{hash}
{runnableVersion}
{isDeployed}
small={useVerticalTriggerBar}
{args}
@@ -416,7 +416,7 @@
{hasPreprocessor}
{canHavePreprocessor}
args={config}
data={{ args, hash, emailDomain }}
data={{ args, hash: !isFlow ? runnableVersion : undefined, emailDomain }}
{isValid}
triggerDeployed={!triggersState.selectedTrigger.isDraft}
on:applyArgs
@@ -24,7 +24,7 @@
initialPath: string
fakeInitialPath: string
currentPath: string
hash?: string
runnableVersion?: string
isDeployed: boolean
small: boolean
args: Record<string, any>
@@ -45,7 +45,7 @@
initialPath,
fakeInitialPath,
currentPath,
hash,
runnableVersion,
small,
args,
newItem,
@@ -68,7 +68,7 @@
<WebhooksPanel
{isFlow}
path={initialPath || fakeInitialPath}
{hash}
{runnableVersion}
token=""
{args}
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
@@ -80,7 +80,7 @@
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
path={initialPath || fakeInitialPath}
{isFlow}
{hash}
runnableVersion={!isFlow ? runnableVersion : undefined}
{onEmailDomain}
/>
{:else if selectedTrigger.type === 'schedule'}
@@ -14,12 +14,12 @@
import UserSettings from '$lib/components/UserSettings.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
let requestType: 'hash' | 'path' = $state('path')
let requestType: 'runnableVersion' | 'path' = $state('path')
function emailAddress() {
const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.')
const pathOrHash = requestType === 'runnableVersion' ? runnableVersion : path.replaceAll('/', '.')
const plainPrefix = `${$workspaceStore}+${
(requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
(requestType === 'runnableVersion' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
}+${token}`
const encodedPrefix = base32
.stringify(new TextEncoder().encode(plainPrefix), {
@@ -32,7 +32,7 @@
interface Props {
token?: string
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
userSettings: UserSettings
emailDomain?: string | null
@@ -42,7 +42,7 @@
let {
token = $bindable(''),
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
userSettings,
emailDomain = null,
@@ -90,7 +90,7 @@
<ToggleButtonGroup class="w-auto" bind:selected={requestType}>
{#snippet children({ item })}
<ToggleButton label="By path" value="path" {item} />
<ToggleButton label="By hash" value="hash" {item} />
<ToggleButton label="By hash" value="runnableVersion" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -16,7 +16,7 @@
token: string
scopes?: string[]
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
onEmailDomain: (domain: string) => void
}
@@ -25,7 +25,7 @@
token = $bindable(),
scopes = [],
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
onEmailDomain
}: Props = $props()
@@ -70,7 +70,7 @@
<Skeleton layout={[[18]]} />
{:else}
{#if emailDomain}
<DefaultEmailConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
<DefaultEmailConfigSection {runnableVersion} {token} {path} {isFlow} {userSettings} {emailDomain} />
{:else}
<div>
<Alert title="Email triggers are disabled" size="xs" type="warning">
@@ -54,6 +54,22 @@ export type TriggerType =
| 'poll'
| 'cli'
export const jobTriggerTypes = [
'webhook',
'http',
'websocket',
'kafka',
'email',
'nats',
'mqtt',
'sqs',
'postgres',
'schedule',
'gcp'
] as const
export type JobTriggerType = (typeof jobTriggerTypes)[number]
export type Trigger = {
type: TriggerType
path?: string
@@ -86,6 +102,23 @@ export const triggerIconMap = {
cli: Terminal
}
export const triggerDisplayNamesMap = {
schedule: 'Schedule',
http: 'HTTP',
websocket: 'WebSocket',
postgres: 'Postgres',
kafka: 'Kafka',
nats: 'NATS',
mqtt: 'MQTT',
sqs: 'SQS',
gcp: 'GCP Pub/Sub',
email: 'Email',
poll: 'Scheduled Poll',
webhook: 'Webhook',
default_email: 'Default Email',
cli: 'CLI'
} as const satisfies Record<TriggerType, string>
/**
* Converts a TriggerType to a CaptureTriggerKind when a mapping exists
* @param triggerType The trigger type to convert
@@ -26,7 +26,7 @@
interface Props {
isFlow?: boolean
path?: string
hash?: string | undefined
runnableVersion?: string | undefined
token?: string
runnableArgs: any
triggerTokens?: TriggerTokens | undefined
@@ -36,46 +36,38 @@
let {
isFlow = false,
path = '',
hash = undefined,
runnableVersion = undefined,
token = $bindable(''),
runnableArgs,
triggerTokens = $bindable(undefined),
scopes = []
}: Props = $props()
let webhooks: {
async: {
get: {}
post: {
hash?: string
path: string
}
const WEBHOOK_BASE_URL = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
let baseWebhookUrl = $derived.by(() => {
let webhookUrlPath: string
if (isFlow) {
webhookUrlPath = runnableId == 'path' ? `f/${path}` : `fv/${runnableVersion}`
} else {
webhookUrlPath = runnableId == 'path' ? `p/${path}` : `h/${runnableVersion}`
}
sync: {
get: {
path: string
}
post: {
hash?: string
path: string
}
if (requestType == 'async') {
return `${WEBHOOK_BASE_URL}/run/${webhookUrlPath}`
} else if (requestType == 'sync') {
return `${WEBHOOK_BASE_URL}/run_wait_result/${webhookUrlPath}`
} else {
return `${WEBHOOK_BASE_URL}/run_and_stream/${webhookUrlPath}`
}
sync_sse: {
get: {
hash?: string
path: string
}
post: {
hash?: string
path: string
}
}
} = $derived(isFlow ? computeFlowWebhooks(path) : computeScriptWebhooks(hash, path))
})
let selectedTab: string = $state('rest')
let userSettings: UserSettings | undefined = $state()
let requestType = $state(DEFAULT_WEBHOOK_TYPE) as 'async' | 'sync' | 'sync_sse'
let callMethod = $state('post') as 'get' | 'post'
let runnableId = $state('path') as 'hash' | 'path'
let runnableId = $state('path') as 'runnableVersion' | 'path'
let tokenType = $state('headers') as 'query' | 'headers'
$effect(() => {
@@ -91,7 +83,7 @@
: runnableArgs
})
let url: string = $derived(
webhooks[requestType][callMethod][runnableId] +
baseWebhookUrl +
(tokenType === 'query'
? `?token=${token}${
callMethod === 'get' || requestType === 'sync_sse'
@@ -105,70 +97,6 @@
}`)
)
function computeScriptWebhooks(hash: string | undefined, path: string) {
let webhookBase = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
return {
async: {
get: {},
post: {
hash: `${webhookBase}/run/h/${hash}`,
path: `${webhookBase}/run/p/${path}`
}
},
sync: {
get: {
path: `${webhookBase}/run_wait_result/p/${path}`
},
post: {
hash: `${webhookBase}/run_wait_result/h/${hash}`,
path: `${webhookBase}/run_wait_result/p/${path}`
}
},
sync_sse: {
get: {
path: `${webhookBase}/run_and_stream/p/${path}`,
hash: `${webhookBase}/run_and_stream/h/${hash}`
},
post: {
hash: `${webhookBase}/run_and_stream/h/${hash}`,
path: `${webhookBase}/run_and_stream/p/${path}`
}
}
}
}
function computeFlowWebhooks(path: string) {
let webhooksBase = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
let urlAsync = `${webhooksBase}/run/f/${path}`
let urlSync = `${webhooksBase}/run_wait_result/f/${path}`
let urlStream = `${webhooksBase}/run_and_stream/f/${path}`
return {
async: {
get: {},
post: {
path: urlAsync
}
},
sync: {
get: {
path: urlSync
},
post: {
path: urlSync
}
},
sync_sse: {
get: {
path: urlStream
},
post: {
path: urlStream
}
}
}
}
function headers() {
const headers = {}
if (callMethod === 'post') {
@@ -433,16 +361,19 @@ done`
{/snippet}
</ToggleButtonGroup>
</Label>
{#if !isFlow}
<Label label="Reference type">
<ToggleButtonGroup bind:selected={runnableId}>
{#snippet children({ item })}
<ToggleButton label="Path" value="path" {item} />
<ToggleButton label="Hash" value="hash" disabled={!hash} {item} />
{/snippet}
</ToggleButtonGroup>
</Label>
{/if}
<Label label="Reference type">
<ToggleButtonGroup bind:selected={runnableId}>
{#snippet children({ item })}
<ToggleButton label="Path" value="path" {item} />
<ToggleButton
label={isFlow ? 'Flow version' : 'Hash'}
value="runnableVersion"
disabled={!runnableVersion}
{item}
/>
{/snippet}
</ToggleButtonGroup>
</Label>
<Label label="Token configuration">
<ToggleButtonGroup bind:selected={tokenType}>
{#snippet children({ item })}
@@ -10,7 +10,7 @@
args?: Record<string, any>
scopes?: string[]
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
newItem?: boolean
}
@@ -20,7 +20,7 @@
args = {},
scopes = [],
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
newItem = false
}: Props = $props()
@@ -39,5 +39,5 @@
{isFlow ? 'flow' : 'script'}.
</Alert>
{/if}
<WebhooksConfigSection {isFlow} {path} {hash} {token} runnableArgs={args} {scopes} />
<WebhooksConfigSection {isFlow} {path} {runnableVersion} {token} runnableArgs={args} {scopes} />
</Section>
@@ -218,6 +218,8 @@
</div>
<div class="flex mt-5 mb-5 gap-1">
<Button
variant="accent"
size="xl"
on:click={() => {
editWindmillLFSSettings()
console.log('Saving S3 settings', s3ResourceSettings)
+29
View File
@@ -0,0 +1,29 @@
/**
* Reads the value of a cookie by name.
* @param {string} name - The name of the cookie to retrieve.
* @returns {string | undefined} The cookie value, or undefined if not found.
*/
export function getCookie(name: string): string | undefined {
const match = document.cookie.match(
new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)')
)
return match ? decodeURIComponent(match[1]) : undefined
}
// this only check the last 2 segments to work for popup on shared top-domain
export function sameTopDomainOrigin(origin: string | null, desktopOrigin: string): boolean {
if (origin == null) {
return false
}
const getLastTwoSegments = (url: string) => {
const parts = url.split('.');
return parts.length >= 2 ? parts.slice(-2).join('.') : url;
};
if (origin.includes('.') && desktopOrigin.includes('.')) {
return getLastTwoSegments(origin) === getLastTwoSegments(desktopOrigin);
} else {
return origin === desktopOrigin;
}
}
@@ -18,9 +18,10 @@
import AuditLogsTimeline from '$lib/components/auditLogs/AuditLogsTimeline.svelte'
let username: string = $state(page.url.searchParams.get('username') ?? 'all')
let pageIndex: number | undefined = $state(Number(page.url.searchParams.get('page')) || 0)
let pageIndex: number | undefined = $state(Number(page.url.searchParams.get('page')) || 1)
let before: string | undefined = $state(page.url.searchParams.get('before') ?? undefined)
let hasMore: boolean = $state(false)
let loading: boolean = $state(false)
let after: string | undefined = $state(page.url.searchParams.get('after') ?? undefined)
let perPage: number | undefined = $state(Number(page.url.searchParams.get('perPage')) || 100)
let operation: string = $state(page.url.searchParams.get('operation') ?? 'all')
@@ -107,6 +108,7 @@
bind:perPage
bind:scope
bind:hasMore
bind:loading
/>
</div>
<div class="2xl:hidden">
@@ -161,6 +163,7 @@
<Pane size={70} minSize={50}>
{#if logs}
<AuditLogsTable
{loading}
{logs}
{selectedId}
bind:pageIndex
@@ -675,6 +675,7 @@
{#if flow}
<TriggersEditor
{args}
runnableVersion={flow.version_id?.toString()}
initialPath={flow.path}
currentPath={flow.path}
noEditor={true}
File diff suppressed because it is too large Load Diff
@@ -791,7 +791,7 @@
{#if script}
<TriggersEditor
{args}
hash={script.hash}
runnableVersion={script.hash}
initialPath={script.path}
currentPath={script.path}
noEditor={true}
+17 -105
View File
@@ -1,33 +1,21 @@
<script lang="ts">
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<EditorBreakpoint>('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')
}
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
>{#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}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
// window.location.reload()
loadApp()
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{workspace}
{notExists}
{noPermission}
{jwtError}
{app}
onLoginSuccess={() => {
loadApp()
}}
></PublicApp>
@@ -1,34 +1,22 @@
<script lang="ts">
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 { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore } 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 Alert from '$lib/components/common/alert/Alert.svelte'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
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 parseSecret(secret: string): { secret: string; jwt: string } {
const parts = secret.split('/')
@@ -59,6 +47,11 @@
if (BROWSER) {
setLicense()
loadAll()
}
function loadAll() {
console.log('loadAll')
loadUser().then(() => {
loadApp()
})
@@ -81,95 +74,15 @@
console.warn('Anonymous user')
}
}
const breakpoint = writable<EditorBreakpoint>('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')
}
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
>{#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}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
console.log('login')
// window.location.reload()
loadUser().then(() => {
loadApp()
})
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{app}
workspace={page.params.workspace}
{notExists}
{noPermission}
{jwtError}
onLoginSuccess={() => {
loadAll()
}}
></PublicApp>
@@ -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) {
+10 -2
View File
@@ -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'

Some files were not shown because too many files have changed in this diff Show More