mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
Merge remote-tracking branch 'origin/main' into di/data-tables
This commit is contained in:
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## [1.575.4](https://github.com/windmill-labs/windmill/compare/v1.575.3...v1.575.4) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix cancel job selection ([d4340b8](https://github.com/windmill-labs/windmill/commit/d4340b83ac94a9dd9febdcfa11288f35c0a57799))
|
||||
|
||||
## [1.575.3](https://github.com/windmill-labs/windmill/compare/v1.575.2...v1.575.3) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix cancel job selection ([fbab86c](https://github.com/windmill-labs/windmill/commit/fbab86cc43dae17727a3aa99abc43a57c6b59b57))
|
||||
|
||||
## [1.575.2](https://github.com/windmill-labs/windmill/compare/v1.575.1...v1.575.2) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix cancel job selection ([1c0f80d](https://github.com/windmill-labs/windmill/commit/1c0f80dc5594146b0ec18caf3b49c036695ce759))
|
||||
|
||||
## [1.575.1](https://github.com/windmill-labs/windmill/compare/v1.575.0...v1.575.1) (2025-11-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix flow buttons clipped in Safari ([#7126](https://github.com/windmill-labs/windmill/issues/7126)) ([77316cb](https://github.com/windmill-labs/windmill/commit/77316cbd0e3ee81fccee9680541f9b2346c8a209))
|
||||
* handle string in input transform strings ([2ee17ae](https://github.com/windmill-labs/windmill/commit/2ee17aea8b29d99c5d2bb609b2becad354835de7))
|
||||
* send alternatives on timeout toast ([#6920](https://github.com/windmill-labs/windmill/issues/6920)) ([6fac896](https://github.com/windmill-labs/windmill/commit/6fac8963909fb40726d85f5e897398d2a691edb0))
|
||||
|
||||
## [1.575.0](https://github.com/windmill-labs/windmill/compare/v1.574.3...v1.575.0) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
-23
@@ -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"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+23
@@ -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"
|
||||
}
|
||||
+23
@@ -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"
|
||||
}
|
||||
+66
@@ -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"
|
||||
}
|
||||
+23
@@ -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"
|
||||
}
|
||||
-66
@@ -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"
|
||||
}
|
||||
+3
-3
@@ -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"
|
||||
}
|
||||
+23
@@ -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"
|
||||
}
|
||||
Generated
+41
-40
@@ -800,9 +800,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.14.1"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
|
||||
checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
@@ -810,9 +810,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.32.3"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c"
|
||||
checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc"
|
||||
dependencies = [
|
||||
"bindgen 0.72.1",
|
||||
"cc",
|
||||
@@ -6739,9 +6739,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.17"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
|
||||
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -10933,9 +10933,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest 0.10.7",
|
||||
@@ -15148,7 +15148,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-config",
|
||||
@@ -15208,7 +15208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15269,6 +15269,7 @@ dependencies = [
|
||||
"pg_escape",
|
||||
"pin-project",
|
||||
"postgres-native-tls 0.5.0",
|
||||
"postgres-native-tls 0.5.1",
|
||||
"prometheus",
|
||||
"quick_cache",
|
||||
"rand 0.9.0",
|
||||
@@ -15328,7 +15329,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -15343,7 +15344,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15357,7 +15358,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15376,7 +15377,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15461,7 +15462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15476,7 +15477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -15500,7 +15501,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15512,7 +15513,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15521,7 +15522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15533,7 +15534,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15545,7 +15546,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15557,7 +15558,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15569,7 +15570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15581,7 +15582,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15592,7 +15593,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15603,7 +15604,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15615,7 +15616,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15638,7 +15639,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15652,7 +15653,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15669,7 +15670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15683,7 +15684,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15701,7 +15702,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15712,7 +15713,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15747,7 +15748,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15757,7 +15758,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -16051,13 +16052,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
authors = ["Ruben Fiszel <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 @@
|
||||
7cdb62d968feef22046b12a1217b711af457cced
|
||||
31ee9d3449f05cd0328c0fcf43e2b161dee767b9
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ impl RunJob {
|
||||
false,
|
||||
None,
|
||||
debounce_job_id_o,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("push has to succeed");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.575.0
|
||||
version: 1.575.4
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -5956,6 +5956,80 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/jobs/run_wait_result/fv/{version}:
|
||||
post:
|
||||
summary: run flow by version and wait until completion
|
||||
operationId: runWaitResultFlowByVersion
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: version
|
||||
description: flow version ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- $ref: "#/components/parameters/QueueLimit"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/SkipPreprocessor"
|
||||
- name: memory_id
|
||||
description: memory ID for chat-enabled flows
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
requestBody:
|
||||
description: script args
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: job result
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
get:
|
||||
summary: run flow by version with GET and wait until completion
|
||||
operationId: runWaitResultFlowByVersionGet
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: version
|
||||
description: flow version ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- $ref: "#/components/parameters/QueueLimit"
|
||||
- $ref: "#/components/parameters/Payload"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/SkipPreprocessor"
|
||||
- name: memory_id
|
||||
description: memory ID for chat-enabled flows
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: job result
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/jobs/run_and_stream/f/{path}:
|
||||
post:
|
||||
summary: run flow by path and stream updates via SSE
|
||||
@@ -6032,6 +6106,94 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs/run_and_stream/fv/{version}:
|
||||
post:
|
||||
summary: run flow by version and stream updates via SSE
|
||||
operationId: runAndStreamFlowByVersion
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: version
|
||||
description: flow version ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- $ref: "#/components/parameters/QueueLimit"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/SkipPreprocessor"
|
||||
- name: memory_id
|
||||
description: memory ID for chat-enabled flows
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: poll_delay_ms
|
||||
description: delay between polling for job updates in milliseconds
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
requestBody:
|
||||
description: flow args
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: server-sent events stream of job updates
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
get:
|
||||
summary: run flow by version with GET and stream updates via SSE
|
||||
operationId: runAndStreamFlowByVersionGet
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: version
|
||||
description: flow version ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- $ref: "#/components/parameters/QueueLimit"
|
||||
- $ref: "#/components/parameters/Payload"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/SkipPreprocessor"
|
||||
- name: memory_id
|
||||
description: memory ID for chat-enabled flows
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: poll_delay_ms
|
||||
description: delay between polling for job updates in milliseconds
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: server-sent events stream of job updates
|
||||
content:
|
||||
text/event-stream:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs/run_and_stream/p/{path}:
|
||||
post:
|
||||
summary: run script by path and stream updates via SSE
|
||||
@@ -7680,6 +7842,64 @@ paths:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
/w/{workspace}/jobs/run/fv/{version}:
|
||||
post:
|
||||
summary: run flow by version
|
||||
operationId: runFlowByVersion
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: version
|
||||
description: flow version ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: scheduled_for
|
||||
description: when to schedule this job (leave empty for immediate run)
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: scheduled_in_secs
|
||||
description: schedule the script to execute in the number of seconds starting now
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- $ref: "#/components/parameters/SkipPreprocessor"
|
||||
- $ref: "#/components/parameters/ParentJob"
|
||||
- $ref: "#/components/parameters/WorkerTag"
|
||||
- $ref: "#/components/parameters/NewJobId"
|
||||
- $ref: "#/components/parameters/IncludeHeader"
|
||||
- name: invisible_to_owner
|
||||
description: make the run invisible to the the flow owner (default false)
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
- name: memory_id
|
||||
description: memory ID for chat-enabled flows
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
description: flow args
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
responses:
|
||||
"201":
|
||||
description: job created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
/w/{workspace}/jobs/run/batch_rerun_jobs:
|
||||
post:
|
||||
summary: re-run multiple jobs
|
||||
@@ -8451,6 +8671,7 @@ paths:
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- $ref: "#/components/parameters/AllowWildcards"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- $ref: "#/components/parameters/JobTriggerKind"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -14424,6 +14645,7 @@ paths:
|
||||
- $ref: "#/components/parameters/AllowWildcards"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- $ref: "#/components/parameters/JobTriggerKind"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -14862,6 +15084,27 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
JobTriggerKind:
|
||||
name: trigger_kind
|
||||
description: trigger kind (schedule, http, websocket...)
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- webhook
|
||||
- default_email
|
||||
- email
|
||||
- schedule
|
||||
- http
|
||||
- websocket
|
||||
- postgres
|
||||
- kafka
|
||||
- nats
|
||||
- mqtt
|
||||
- sqs
|
||||
- gcp
|
||||
- poll
|
||||
- cli
|
||||
OrderDesc:
|
||||
name: order_desc
|
||||
description: order by desc order (default true)
|
||||
@@ -18698,6 +18941,8 @@ components:
|
||||
properties:
|
||||
lock_error_logs:
|
||||
type: string
|
||||
version_id:
|
||||
type: number
|
||||
|
||||
ExtraPerms:
|
||||
type: object
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -562,6 +562,7 @@ async fn create_flow(
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1025,6 +1026,7 @@ async fn update_flow(
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1133,6 +1135,7 @@ async fn get_flow_by_path(
|
||||
flow.timeout,
|
||||
flow.visible_to_runner_only,
|
||||
flow.on_behalf_of_email,
|
||||
flow_version.id AS version_id,
|
||||
flow_version.schema,
|
||||
flow_version.value,
|
||||
flow_version.created_at AS edited_at,
|
||||
@@ -1172,8 +1175,9 @@ async fn get_flow_by_path(
|
||||
flow.timeout,
|
||||
flow.visible_to_runner_only,
|
||||
flow.on_behalf_of_email,
|
||||
flow_version.id AS version_id,
|
||||
flow_version.schema,
|
||||
flow_version.value,
|
||||
flow_version.value,
|
||||
flow_version.created_at AS edited_at,
|
||||
flow_version.created_by AS edited_by,
|
||||
NULL AS starred
|
||||
|
||||
+402
-146
@@ -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",
|
||||
];
|
||||
|
||||
|
||||
@@ -1022,6 +1022,7 @@ async fn create_script_internal<'c>(
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, new_tx, None))
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>>(
|
||||
|
||||
@@ -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 {
|
||||
@@ -5277,11 +5282,13 @@ pub async fn push<'c, 'd>(
|
||||
// tracing::error!("Could not insert job_perms for job {job_id}: {err:#}");
|
||||
// }
|
||||
|
||||
let trigger_kind = if schedule_path.is_some() {
|
||||
Some(JobTriggerKind::Schedule)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let trigger_kind = trigger_kind.or_else(|| {
|
||||
if schedule_path.is_some() {
|
||||
Some(JobTriggerKind::Schedule)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let root_job = if root_job.is_some()
|
||||
&& (root_job == flow_innermost_root_job.or(parent_job).or(Some(job_id)))
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -471,6 +471,7 @@ async fn execute_windmill_tool(
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.575.0";
|
||||
export const VERSION = "v1.575.4";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.575.0";
|
||||
export const VERSION = "1.575.4";
|
||||
|
||||
export const WM_FORK_PREFIX = "wm-fork";
|
||||
|
||||
|
||||
Generated
+6
-29
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.575.0",
|
||||
"version": "1.575.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.575.0",
|
||||
"version": "1.575.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@scalar/openapi-parser": "^0.15.0",
|
||||
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
|
||||
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
|
||||
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.16",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.44",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xyflow/svelte": "^1.0.0",
|
||||
@@ -3020,9 +3020,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tutorlatin/svelte-tiny-virtual-list": {
|
||||
"version": "3.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.15.tgz",
|
||||
"integrity": "sha512-ew61aZNXGf0b5X+UjbOAhiNwzI21vijhB/mtBs8bpNOVYQ50TG6Qx00t+fR5C72eGnmdzguewZ2WP6QPNOTQJg==",
|
||||
"version": "3.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.16.tgz",
|
||||
"integrity": "sha512-JQSmhRDAFZbq2rTlzn+kFXJayi5VPLxeGjD01ZVyV2ti7PlQE/ov6rQFR1c8s7Y3B1OiTcv3oEWGi3ib69V8eQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.17.0"
|
||||
@@ -13936,29 +13936,6 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.575.0",
|
||||
"version": "1.575.4",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -89,7 +89,7 @@
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@scalar/openapi-parser": "^0.15.0",
|
||||
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
|
||||
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
|
||||
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.16",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.44",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xyflow/svelte": "^1.0.0",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <WindmillIcon /> 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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 <WindmillIcon /> 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 <WindmillIcon /> 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
@@ -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'
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.575.0"
|
||||
wmill_pg = ">=1.575.0"
|
||||
wmill = ">=1.575.4"
|
||||
wmill_pg = ">=1.575.4"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.575.0
|
||||
version: 1.575.4
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.575.0'
|
||||
ModuleVersion = '1.575.4'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.575.0"
|
||||
version = "1.575.4"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.575.0",
|
||||
"version": "1.575.4",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.575.0",
|
||||
"version": "1.575.4",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.575.0
|
||||
1.575.4
|
||||
|
||||
Reference in New Issue
Block a user