mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
Merge branch 'main' into di/s3-proxy
This commit is contained in:
@@ -1,5 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
## [1.538.0](https://github.com/windmill-labs/windmill/compare/v1.537.1...v1.538.0) (2025-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** cache prompts when using anthropic models ([#6489](https://github.com/windmill-labs/windmill/issues/6489)) ([8b3ae19](https://github.com/windmill-labs/windmill/commit/8b3ae1984c745164bbf3938e6f8fc3cf02a0e2c1))
|
||||
* **frontend:** preprocessor snippet completion in code editor ([#6502](https://github.com/windmill-labs/windmill/issues/6502)) ([a5a9a33](https://github.com/windmill-labs/windmill/commit/a5a9a33d10fcac802834b1c046a3c116ab5b3454))
|
||||
* **perf:** add 300-second local cache for variable crypt retrieval ([#6483](https://github.com/windmill-labs/windmill/issues/6483)) ([8f46066](https://github.com/windmill-labs/windmill/commit/8f46066a6478a48411e00f5c4e1f9b9fdb49c1fc))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** http triggers early return ([#6501](https://github.com/windmill-labs/windmill/issues/6501)) ([c01ed2d](https://github.com/windmill-labs/windmill/commit/c01ed2d62a440b0ade624ed1def6bb2a54f5aa81))
|
||||
* **frontend:** capture/trigger UI nits ([#6494](https://github.com/windmill-labs/windmill/issues/6494)) ([8cd1c64](https://github.com/windmill-labs/windmill/commit/8cd1c6474b3c55d706683f83b40ef5569245b559))
|
||||
* **frontend:** Improve runs page ux ([#6485](https://github.com/windmill-labs/windmill/issues/6485)) ([768c600](https://github.com/windmill-labs/windmill/commit/768c60049413277e2764ef889274d4f8691412aa))
|
||||
* **frontend:** make resource a separate top-level type in schema editor for clarity ([0f6a742](https://github.com/windmill-labs/windmill/commit/0f6a74256dba0f571e9fcfa876e78971385ba541))
|
||||
* schema editor reactivity improvements ([#6496](https://github.com/windmill-labs/windmill/issues/6496)) ([6e46058](https://github.com/windmill-labs/windmill/commit/6e46058c5a9af6cd9fb8c1f08bb58dc123d5323e))
|
||||
|
||||
## [1.537.1](https://github.com/windmill-labs/windmill/compare/v1.537.0...v1.537.1) (2025-08-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix error handling of pre-processor steps ([641d565](https://github.com/windmill-labs/windmill/commit/641d5651c5b6fa898e4fc8ad29a8508f9400c6d8))
|
||||
* fix preprocessor not displaying immediately on addition ([e28c9df](https://github.com/windmill-labs/windmill/commit/e28c9df60f20b70a4dcef802839750bb843b91a4))
|
||||
* skipPreprocessor on re-running job immedaitely from UI ([e2b344e](https://github.com/windmill-labs/windmill/commit/e2b344ed02b02972366a011c3e873e0f43ab2843))
|
||||
|
||||
## [1.537.0](https://github.com/windmill-labs/windmill/compare/v1.536.0...v1.537.0) (2025-08-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** allow reverting specific line for inline script suggestions ([#6480](https://github.com/windmill-labs/windmill/issues/6480)) ([0cc11b3](https://github.com/windmill-labs/windmill/commit/0cc11b3f31aeee60a9d4a231cea5d4285d7ab37e))
|
||||
* autovacuum or high intensity tables ([4ad0d25](https://github.com/windmill-labs/windmill/commit/4ad0d255f3eea303e97eab5325f89930b26f9e52))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix okta and oauth0 sso settings ([73a3f4c](https://github.com/windmill-labs/windmill/commit/73a3f4cc73271759650e9246f4eb2e0efb7c7e37))
|
||||
* fix relative imports cache invalidation ([#6468](https://github.com/windmill-labs/windmill/issues/6468)) ([006f326](https://github.com/windmill-labs/windmill/commit/006f32602c7609b282f15135989c5f164c109c1c))
|
||||
* fix workflow as code behavior with multithread ([4973c86](https://github.com/windmill-labs/windmill/commit/4973c860f2c28d9bdc2af94530d90eb177234e5d))
|
||||
|
||||
## [1.536.0](https://github.com/windmill-labs/windmill/compare/v1.535.0...v1.536.0) (2025-08-27)
|
||||
|
||||
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets) \n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets \n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0156016836adeb2714d99811e4ad2ea9245f05e772f3601c99312c13ed65ae1a"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets) \n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, timeout, concurrency_key, visible_to_runner_only, no_main_func, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets \n\n FROM script WHERE hash = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03e213d2934991c57af64b5ae94a6aa59599f852cc17b16323fc627b6ad8671e"
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n w.premium,\n COALESCE(cw.is_past_due, false) as \"is_past_due!\",\n cw.max_tolerated_executions\n FROM\n workspace w\n LEFT JOIN cloud_workspace_settings cw ON cw.workspace_id = w.id\n WHERE\n w.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "premium",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "is_past_due!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "max_tolerated_executions",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "124e67b0cee1baa6295846db4ad6242a39dd40186f1dbb48ad3018bd9f6913ec"
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT owner, premium, usage.usage as \"usage?\", workspace_settings.customer_id, workspace_settings.plan FROM workspace LEFT JOIN workspace_settings ON workspace_settings.workspace_id = $1 LEFT JOIN usage ON usage.id = $1 AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date) AND usage.is_workspace IS true WHERE workspace.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "premium",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "usage?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "customer_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "plan",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4b8132b04e454eddfe6724c6cc3a2e60c9c24decb2a6b41125247bbf741e9c25"
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n owner, \n premium, \n usage.usage as \"usage?\", \n workspace_settings.customer_id, \n workspace_settings.plan, \n COALESCE(cw.is_past_due, false) as \"is_past_due!\", \n cw.max_tolerated_executions\n FROM workspace\n LEFT JOIN workspace_settings \n ON workspace_settings.workspace_id = $1\n LEFT JOIN usage \n ON usage.id = $1\n AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND usage.is_workspace IS true\n LEFT JOIN cloud_workspace_settings cw\n ON cw.workspace_id = $1\n WHERE workspace.id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "owner",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "premium",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "usage?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "customer_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "plan",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "is_past_due!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "max_tolerated_executions",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ac4a375035049304db7ae084b2d1789afb8250adde6f38f458b115e787ed876f"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT premium FROM workspace WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "premium",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d768bbc46f8a9c4289b918c88ca50aa180b29bbe931d948f6e61976f71b7cdb9"
|
||||
}
|
||||
Generated
+86
-114
@@ -788,9 +788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.2.5"
|
||||
version = "1.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1541072f81945fa1251f8795ef6c92c4282d74d59f88498ae7d4bf00f0ebdad9"
|
||||
checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1040,9 +1040,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.61.4"
|
||||
version = "0.61.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9"
|
||||
checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
]
|
||||
@@ -1917,10 +1917,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.34"
|
||||
version = "1.2.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc"
|
||||
checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
@@ -2127,9 +2128,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "7.1.4"
|
||||
version = "7.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a"
|
||||
checksum = "3f8e18d0dca9578507f13f9803add0df13362b02c501c1c17734f0dbb52eaf0b"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.1",
|
||||
@@ -4471,9 +4472,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.4.0"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
@@ -5152,7 +5153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
dependencies = [
|
||||
"bit-set 0.5.3",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.8.6",
|
||||
]
|
||||
|
||||
@@ -5163,7 +5164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.8.6",
|
||||
]
|
||||
|
||||
@@ -5262,6 +5263,12 @@ dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
@@ -5855,9 +5862,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.23"
|
||||
version = "0.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width 0.2.1",
|
||||
]
|
||||
@@ -5885,7 +5892,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasi 0.14.3+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -5951,7 +5958,7 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.8.6",
|
||||
]
|
||||
|
||||
@@ -6899,9 +6906,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "if_chain"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
|
||||
checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb"
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
@@ -6913,7 +6920,7 @@ dependencies = [
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
@@ -7833,7 +7840,7 @@ version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53304fff6ab1e597661eee37e42ea8c47a146fca280af902bb76bff8a896e523"
|
||||
dependencies = [
|
||||
"nu-ansi-term 0.50.1",
|
||||
"nu-ansi-term",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8010,11 +8017,11 @@ checksum = "204651f31b0a6a7b2128d2b92c372cd94607b210c3a6b6e542c57a8cfd4db996"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8603,16 +8610,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
@@ -9315,12 +9312,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "ownedbytes"
|
||||
version = "0.9.0"
|
||||
@@ -9899,9 +9890,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
|
||||
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
@@ -10275,9 +10266,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.8"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases 0.2.1",
|
||||
@@ -10286,7 +10277,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls 0.23.29",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.0",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -10295,9 +10286,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.12"
|
||||
version = "0.11.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e"
|
||||
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.3",
|
||||
@@ -10316,16 +10307,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.13"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.0",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10635,19 +10626,10 @@ checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.10"
|
||||
@@ -10665,12 +10647,6 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
@@ -13532,12 +13508,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.41"
|
||||
version = "0.3.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
checksum = "8ca967379f9d8eb8058d86ed467d81d03e81acd45757e4ca341c24affbe8e8e3"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
@@ -13547,15 +13522,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.4"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
checksum = "a9108bb380861b07264b950ded55a44a14a4adc68b9f5efd85aafc3aa4d40a68"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
checksum = "7182799245a7264ce590b349d90338f1c1affad93d2639aed5f8f69c090b334c"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@@ -14178,14 +14153,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term 0.46.0",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
@@ -14793,11 +14768,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
version = "0.14.3+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15152,7 +15127,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-config",
|
||||
@@ -15208,7 +15183,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15327,7 +15302,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -15342,7 +15317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -15355,7 +15330,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15374,7 +15349,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15455,7 +15430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15470,7 +15445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -15494,7 +15469,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15506,7 +15481,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15515,7 +15490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15527,7 +15502,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15539,7 +15514,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15551,7 +15526,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15563,7 +15538,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15575,7 +15550,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15586,7 +15561,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15597,7 +15572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15609,7 +15584,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15632,7 +15607,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15646,7 +15621,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15663,7 +15638,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15677,7 +15652,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15695,7 +15670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.16",
|
||||
@@ -15720,7 +15695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15730,7 +15705,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15763,7 +15738,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15773,7 +15748,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16353,13 +16328,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
name = "wit-bindgen"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.536.0"
|
||||
version = "1.538.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
6396854336ae27fb14ccb792d80c31ff614b2afa
|
||||
d16e52d570f10dfdabb04a9061fe7ebdbf5a2aa1
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Add down migration script here
|
||||
DROP FUNCTION notify_team_plan_status_change;
|
||||
DROP TRIGGER notify_team_plan_status_change ON cloud_workspace_settings;
|
||||
|
||||
ALTER TABLE cloud_workspace_settings
|
||||
DROP COLUMN is_past_due,
|
||||
DROP COLUMN max_tolerated_executions;
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE cloud_workspace_settings
|
||||
ADD COLUMN is_past_due BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN max_tolerated_executions INTEGER;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_team_plan_status_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('notify_workspace_premium_change', NEW.workspace_id); -- reuse the same channel as the one used for workspace premium change => clear cache
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
CREATE TRIGGER team_plan_status_change_trigger
|
||||
AFTER UPDATE OF is_past_due, max_tolerated_executions ON cloud_workspace_settings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_team_plan_status_change();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Remove workspace key cache invalidation trigger
|
||||
|
||||
DROP TRIGGER workspace_key_change_trigger ON workspace_key;
|
||||
DROP FUNCTION notify_workspace_key_change();
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Add workspace key cache invalidation trigger
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_workspace_key_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
PERFORM pg_notify('notify_workspace_key_change', OLD.workspace_id);
|
||||
RETURN OLD;
|
||||
ELSE
|
||||
PERFORM pg_notify('notify_workspace_key_change', NEW.workspace_id);
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER workspace_key_change_trigger
|
||||
AFTER INSERT OR UPDATE OF key OR DELETE ON workspace_key
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_workspace_key_change();
|
||||
+7
-1
@@ -854,10 +854,15 @@ Windmill Community Edition {GIT_VERSION}
|
||||
tracing::info!("Workspace envs change detected, invalidating workspace envs cache: {}", workspace_id);
|
||||
windmill_common::variables::CUSTOM_ENVS_CACHE.remove(workspace_id);
|
||||
},
|
||||
"notify_workspace_key_change" => {
|
||||
let workspace_id = n.payload();
|
||||
tracing::info!("Workspace key change detected, invalidating workspace key cache: {}", workspace_id);
|
||||
windmill_common::variables::WORKSPACE_CRYPT_CACHE.remove(workspace_id);
|
||||
},
|
||||
"notify_workspace_premium_change" => {
|
||||
let workspace_id = n.payload();
|
||||
tracing::info!("Workspace premium change detected, invalidating workspace premium cache: {}", workspace_id);
|
||||
windmill_common::workspaces::IS_PREMIUM_CACHE.remove(workspace_id);
|
||||
windmill_common::workspaces::TEAM_PLAN_CACHE.remove(workspace_id);
|
||||
},
|
||||
"notify_runnable_version_change" => {
|
||||
let payload = n.payload();
|
||||
@@ -1268,6 +1273,7 @@ async fn listen_pg(url: &str) -> Option<PgListener> {
|
||||
"notify_global_setting_change",
|
||||
"notify_webhook_change",
|
||||
"notify_workspace_envs_change",
|
||||
"notify_workspace_key_change",
|
||||
"notify_runnable_version_change",
|
||||
"notify_token_invalidation",
|
||||
];
|
||||
|
||||
@@ -149,7 +149,9 @@ lazy_static::lazy_static! {
|
||||
static ref QUEUE_RUNNING_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
static ref DISABLE_CONCURRENCY_LIMIT: bool = std::env::var("DISABLE_CONCURRENCY_LIMIT").is_ok_and(|s| s == "true");
|
||||
|
||||
static ref STALE_JOB_TRESHOLD_MINUTES: Option<u64> = std::env::var("STALE_JOB_TRESHOLD_MINUTES")
|
||||
//legacy typo
|
||||
static ref STALE_JOB_THRESHOLD_MINUTES: Option<u64> = std::env::var("STALE_JOB_THRESHOLD_MINUTES").or_else(|_| std::env::var("STALE_JOB_THRESHOLD_MINUTES"))
|
||||
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u64>().ok());
|
||||
}
|
||||
@@ -1896,7 +1898,7 @@ pub async fn reload_base_url_setting(conn: &Connection) -> error::Result<()> {
|
||||
}
|
||||
|
||||
async fn stale_job_cancellation(db: &Pool<Postgres>) {
|
||||
if let Some(threshold) = *STALE_JOB_TRESHOLD_MINUTES {
|
||||
if let Some(threshold) = *STALE_JOB_THRESHOLD_MINUTES {
|
||||
let stale_jobs = sqlx::query!(
|
||||
"SELECT v2_job_queue.id, v2_job.tag, v2_job_queue.scheduled_for, v2_job_queue.workspace_id FROM v2_job_queue LEFT JOIN v2_job ON v2_job_queue.id = v2_job.id WHERE running = false AND scheduled_for < now() - ($1 || ' minutes')::interval",
|
||||
threshold.to_string()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.536.0
|
||||
version: 1.538.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -2034,6 +2034,11 @@ paths:
|
||||
- workspace
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: skip_subscription_fetch
|
||||
in: query
|
||||
description: skip fetching subscription status from stripe
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
responses:
|
||||
"200":
|
||||
@@ -2051,9 +2056,14 @@ paths:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
is_past_due:
|
||||
type: boolean
|
||||
max_tolerated_executions:
|
||||
type: number
|
||||
required:
|
||||
- premium
|
||||
- owner
|
||||
- is_past_due
|
||||
|
||||
/w/{workspace}/workspaces/threshold_alert:
|
||||
get:
|
||||
|
||||
@@ -156,6 +156,7 @@ impl AIRequestConfig {
|
||||
let is_azure = matches!(provider, AIProvider::OpenAI) && base_url != OPENAI_BASE_URL
|
||||
|| matches!(provider, AIProvider::AzureOpenAI);
|
||||
let is_anthropic = matches!(provider, AIProvider::Anthropic);
|
||||
let is_anthropic_sdk = headers.get("X-Anthropic-SDK").is_some();
|
||||
|
||||
let url = if is_azure && method != Method::GET {
|
||||
if base_url.ends_with("/deployments") {
|
||||
@@ -167,6 +168,9 @@ impl AIRequestConfig {
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
}
|
||||
} else if is_anthropic_sdk {
|
||||
let truncated_base_url = base_url.trim_end_matches("/v1");
|
||||
format!("{}/{}", truncated_base_url, path)
|
||||
} else {
|
||||
format!("{}/{}", base_url, path)
|
||||
};
|
||||
|
||||
@@ -3741,7 +3741,7 @@ async fn batch_rerun_handle_job(
|
||||
PushArgsOwned { extra: None, args },
|
||||
)
|
||||
.await;
|
||||
if let Ok(uuid) = result {
|
||||
if let Ok((uuid, _)) = result {
|
||||
return Ok(uuid.to_string());
|
||||
}
|
||||
}
|
||||
@@ -3798,7 +3798,7 @@ pub async fn run_flow_by_path(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let uuid =
|
||||
let (uuid, _) =
|
||||
run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args).await?;
|
||||
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
@@ -3812,7 +3812,7 @@ pub async fn run_flow_by_path_inner(
|
||||
flow_path: StripPath,
|
||||
run_query: RunJobQuery,
|
||||
args: PushArgsOwned,
|
||||
) -> error::Result<Uuid> {
|
||||
) -> error::Result<(Uuid, Option<String>)> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
|
||||
@@ -3828,6 +3828,7 @@ pub async fn run_flow_by_path_inner(
|
||||
has_preprocessor,
|
||||
on_behalf_of_email,
|
||||
edited_by,
|
||||
early_return,
|
||||
..
|
||||
} = get_latest_flow_version_info_for_path(&mut *tx, &w_id, &flow_path, true).await?;
|
||||
|
||||
@@ -3889,7 +3890,7 @@ pub async fn run_flow_by_path_inner(
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(uuid)
|
||||
Ok((uuid, early_return))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
|
||||
@@ -472,12 +472,12 @@ async fn trigger_runnable_inner(
|
||||
error_handler_path: Option<&str>,
|
||||
error_handler_args: Option<&sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
|
||||
trigger_path: String,
|
||||
) -> Result<(Uuid, Option<bool>)> {
|
||||
) -> Result<(Uuid, Option<bool>, Option<String>)> {
|
||||
let user_db = user_db.unwrap_or_else(|| UserDB::new(db.clone()));
|
||||
let (uuid, delete_after_use) = if is_flow {
|
||||
let (uuid, delete_after_use, early_return) = if is_flow {
|
||||
let run_query = RunJobQuery::default();
|
||||
let path = StripPath(runnable_path.to_string());
|
||||
let uuid = run_flow_by_path_inner(
|
||||
let (uuid, early_return) = run_flow_by_path_inner(
|
||||
authed,
|
||||
db.clone(),
|
||||
user_db,
|
||||
@@ -487,9 +487,9 @@ async fn trigger_runnable_inner(
|
||||
args,
|
||||
)
|
||||
.await?;
|
||||
(uuid, None)
|
||||
(uuid, None, early_return)
|
||||
} else {
|
||||
trigger_script_internal(
|
||||
let (uuid, delete_after_use) = trigger_script_internal(
|
||||
db,
|
||||
user_db,
|
||||
authed,
|
||||
@@ -501,10 +501,11 @@ async fn trigger_runnable_inner(
|
||||
error_handler_args,
|
||||
trigger_path,
|
||||
)
|
||||
.await?
|
||||
.await?;
|
||||
(uuid, delete_after_use, None)
|
||||
};
|
||||
|
||||
Ok((uuid, delete_after_use))
|
||||
Ok((uuid, delete_after_use, early_return))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -521,7 +522,7 @@ pub async fn trigger_runnable(
|
||||
error_handler_args: Option<&sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
|
||||
trigger_path: String,
|
||||
) -> Result<axum::response::Response> {
|
||||
let (uuid, _) = trigger_runnable_inner(
|
||||
let (uuid, _, _) = trigger_runnable_inner(
|
||||
db,
|
||||
user_db,
|
||||
authed,
|
||||
@@ -553,7 +554,7 @@ pub async fn trigger_runnable_and_wait_for_result(
|
||||
trigger_path: String,
|
||||
) -> Result<axum::response::Response> {
|
||||
let username = authed.username.clone();
|
||||
let (uuid, delete_after_use) = trigger_runnable_inner(
|
||||
let (uuid, delete_after_use, early_return) = trigger_runnable_inner(
|
||||
db,
|
||||
user_db,
|
||||
authed,
|
||||
@@ -568,7 +569,8 @@ pub async fn trigger_runnable_and_wait_for_result(
|
||||
)
|
||||
.await?;
|
||||
let (result, success) =
|
||||
run_wait_result_internal(db, uuid, workspace_id.to_string(), None, &username).await?;
|
||||
run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username)
|
||||
.await?;
|
||||
|
||||
if delete_after_use.unwrap_or(false) {
|
||||
delete_job_metadata_after_use(&db, uuid).await?;
|
||||
@@ -592,7 +594,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result(
|
||||
trigger_path: String,
|
||||
) -> Result<Box<RawValue>> {
|
||||
let username = authed.username.clone();
|
||||
let (uuid, delete_after_use) = trigger_runnable_inner(
|
||||
let (uuid, delete_after_use, early_return) = trigger_runnable_inner(
|
||||
db,
|
||||
user_db,
|
||||
authed,
|
||||
@@ -607,23 +609,6 @@ pub async fn trigger_runnable_and_wait_for_raw_result(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let early_return = if is_flow {
|
||||
sqlx::query_scalar!(
|
||||
r#"SELECT flow_version.value->>'early_return' as early_return
|
||||
FROM flow
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 and flow.workspace_id = $2"#,
|
||||
runnable_path,
|
||||
workspace_id,
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (result, success) =
|
||||
run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username)
|
||||
.await
|
||||
|
||||
@@ -35,7 +35,7 @@ use windmill_audit::ActionKind;
|
||||
use windmill_common::db::UserDB;
|
||||
use windmill_common::s3_helpers::LargeFileStorage;
|
||||
use windmill_common::users::username_to_permissioned_as;
|
||||
use windmill_common::variables::{build_crypt, decrypt, encrypt};
|
||||
use windmill_common::variables::{build_crypt, decrypt, encrypt, WORKSPACE_CRYPT_CACHE};
|
||||
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
|
||||
#[cfg(feature = "enterprise")]
|
||||
use windmill_common::workspaces::GitRepositorySettings;
|
||||
@@ -415,7 +415,9 @@ async fn is_premium(
|
||||
) -> JsonResult<bool> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
#[cfg(feature = "cloud")]
|
||||
let premium = windmill_common::workspaces::is_premium_workspace(&_db, &_w_id).await;
|
||||
let premium = windmill_common::workspaces::get_team_plan_status(&_db, &_w_id)
|
||||
.await
|
||||
.premium;
|
||||
#[cfg(not(feature = "cloud"))]
|
||||
let premium = false;
|
||||
Ok(Json(premium))
|
||||
@@ -1926,6 +1928,8 @@ async fn set_encryption_key(
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
WORKSPACE_CRYPT_CACHE.remove(w_id.as_str());
|
||||
|
||||
if !request.skip_reencrypt.unwrap_or(false) {
|
||||
let new_encryption_key = build_crypt(&db, w_id.as_str()).await?;
|
||||
|
||||
@@ -1948,7 +1952,13 @@ async fn set_encryption_key(
|
||||
if !variable.is_secret {
|
||||
continue;
|
||||
}
|
||||
let decrypted_value = decrypt(&previous_encryption_key, variable.value)?;
|
||||
let decrypted_value =
|
||||
decrypt(&previous_encryption_key, variable.value).map_err(|e| {
|
||||
Error::internal_err(format!(
|
||||
"Error decrypting variable {}: {}",
|
||||
variable.path, e
|
||||
))
|
||||
})?;
|
||||
let new_encrypted_value = encrypt(&new_encryption_key, decrypted_value.as_str());
|
||||
sqlx::query!(
|
||||
"UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3",
|
||||
|
||||
@@ -654,6 +654,12 @@ pub async fn get_logs_from_store(
|
||||
return None;
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TAGS_ARE_SENSITIVE: bool = std::env::var("TAGS_ARE_SENSITIVE").map(
|
||||
|v| v.parse().unwrap()
|
||||
).unwrap_or(false);
|
||||
}
|
||||
|
||||
pub async fn check_tag_available_for_workspace_internal(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
@@ -691,10 +697,14 @@ pub async fn check_tag_available_for_workspace_internal(
|
||||
)));
|
||||
}
|
||||
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
if *TAGS_ARE_SENSITIVE {
|
||||
return Err(Error::BadRequest(format!("{tag} is not available to you")));
|
||||
} else {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Only super admins are allowed to use tags that are not included in the allowed CUSTOM_TAGS: {:?}",
|
||||
custom_tags_per_w
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
|
||||
@@ -78,13 +78,38 @@ pub struct CreateVariable {
|
||||
}
|
||||
|
||||
pub async fn build_crypt(db: &DB, w_id: &str) -> crate::error::Result<MagicCrypt256> {
|
||||
let key = get_workspace_key(w_id, db).await?;
|
||||
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
|
||||
format!("{}{}", key, salt)
|
||||
// Check cache first (300-second staleness)
|
||||
let cached_key_o = WORKSPACE_CRYPT_CACHE.get(w_id).and_then(|(ts, key)| {
|
||||
if ts > chrono::Utc::now().timestamp() - 300 {
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let crypt = if let Some(cached_key) = cached_key_o {
|
||||
cached_key
|
||||
} else {
|
||||
key
|
||||
let key = get_workspace_key(w_id, db).await?;
|
||||
tracing::info!(
|
||||
"crypt for workspace {} with key {} expired, refetching",
|
||||
w_id,
|
||||
key
|
||||
);
|
||||
let crypt_key = if let Some(ref salt) = SECRET_SALT.as_ref() {
|
||||
format!("{}{}", key, salt)
|
||||
} else {
|
||||
key
|
||||
};
|
||||
let ncrypt = magic_crypt::new_magic_crypt!(crypt_key, 256);
|
||||
WORKSPACE_CRYPT_CACHE.insert(
|
||||
w_id.to_string(),
|
||||
(chrono::Utc::now().timestamp(), ncrypt.clone()),
|
||||
);
|
||||
ncrypt
|
||||
};
|
||||
Ok(magic_crypt::new_magic_crypt!(crypt_key, 256))
|
||||
|
||||
Ok(crypt)
|
||||
}
|
||||
|
||||
pub async fn build_crypt_with_key_suffix(
|
||||
@@ -110,6 +135,7 @@ pub async fn get_workspace_key(w_id: &str, db: &DB) -> crate::error::Result<Stri
|
||||
.warn_after_seconds(5)
|
||||
.await
|
||||
.map_err(|e| crate::Error::internal_err(format!("fetching workspace key: {e:#}")))?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
@@ -166,6 +192,8 @@ pub const WM_SCHEDULED_FOR: &str = "WM_SCHEDULED_FOR";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CUSTOM_ENVS_CACHE: Cache<String, (i64, Vec<(String, String)>)> = Cache::new(100);
|
||||
pub static ref WORKSPACE_CRYPT_CACHE: Cache<String, (i64, MagicCrypt256)> = Cache::new(1000);
|
||||
|
||||
}
|
||||
|
||||
pub async fn get_reserved_variables(
|
||||
|
||||
@@ -82,22 +82,47 @@ impl Default for GitSyncSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TeamPlanStatus {
|
||||
pub premium: bool,
|
||||
pub is_past_due: bool,
|
||||
pub max_tolerated_executions: Option<i32>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref IS_PREMIUM_CACHE: Cache<String, bool> = Cache::new(5000);
|
||||
pub static ref TEAM_PLAN_CACHE: Cache<String, TeamPlanStatus> = Cache::new(5000);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cloud")]
|
||||
pub async fn is_premium_workspace(_db: &crate::DB, _w_id: &str) -> bool {
|
||||
let cached = IS_PREMIUM_CACHE.get(_w_id);
|
||||
pub async fn get_team_plan_status(_db: &crate::DB, _w_id: &str) -> TeamPlanStatus {
|
||||
let cached = TEAM_PLAN_CACHE.get(_w_id);
|
||||
if let Some(cached) = cached {
|
||||
return cached;
|
||||
}
|
||||
let premium = sqlx::query_scalar!("SELECT premium FROM workspace WHERE id = $1", _w_id)
|
||||
.fetch_one(_db)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
IS_PREMIUM_CACHE.insert(_w_id.to_string(), premium);
|
||||
premium
|
||||
let team_plan_info = sqlx::query_as!(
|
||||
TeamPlanStatus,
|
||||
r#"
|
||||
SELECT
|
||||
w.premium,
|
||||
COALESCE(cw.is_past_due, false) as "is_past_due!",
|
||||
cw.max_tolerated_executions
|
||||
FROM
|
||||
workspace w
|
||||
LEFT JOIN cloud_workspace_settings cw ON cw.workspace_id = w.id
|
||||
WHERE
|
||||
w.id = $1
|
||||
"#,
|
||||
_w_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await
|
||||
.unwrap_or_else(|_| TeamPlanStatus {
|
||||
premium: false,
|
||||
is_past_due: false,
|
||||
max_tolerated_executions: None,
|
||||
});
|
||||
TEAM_PLAN_CACHE.insert(_w_id.to_string(), team_plan_info.clone());
|
||||
team_plan_info
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
|
||||
@@ -1419,8 +1419,9 @@ fn apply_completed_job_cloud_usage(
|
||||
let email2 = email.clone();
|
||||
tokio::task::spawn(async move {
|
||||
let additional_usage = _duration / 1000;
|
||||
let premium_workspace =
|
||||
windmill_common::workspaces::is_premium_workspace(&db, &w_id).await;
|
||||
let premium_workspace = windmill_common::workspaces::get_team_plan_status(&db, &w_id)
|
||||
.await
|
||||
.premium;
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async move {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
@@ -3636,8 +3637,8 @@ pub async fn push<'c, 'd>(
|
||||
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
|
||||
#[cfg(feature = "cloud")]
|
||||
if *CLOUD_HOSTED {
|
||||
let premium_workspace =
|
||||
windmill_common::workspaces::is_premium_workspace(_db, workspace_id).await;
|
||||
let team_plan_status =
|
||||
windmill_common::workspaces::get_team_plan_status(_db, workspace_id).await;
|
||||
// we track only non flow steps
|
||||
let (workspace_usage, user_usage) = if !matches!(
|
||||
job_payload,
|
||||
@@ -3655,7 +3656,7 @@ pub async fn push<'c, 'd>(
|
||||
.await
|
||||
.map_err(|e| Error::internal_err(format!("updating usage: {e:#}")))?;
|
||||
|
||||
let user_usage = if !premium_workspace {
|
||||
let user_usage = if !team_plan_status.premium {
|
||||
Some(sqlx::query_scalar!(
|
||||
"INSERT INTO usage (id, is_workspace, month_, usage)
|
||||
VALUES ($1, FALSE, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 1)
|
||||
@@ -3678,7 +3679,7 @@ pub async fn push<'c, 'd>(
|
||||
Ok((None, None))
|
||||
}?;
|
||||
|
||||
if !premium_workspace {
|
||||
if !team_plan_status.premium || team_plan_status.is_past_due {
|
||||
let is_super_admin =
|
||||
sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||
.fetch_optional(_db)
|
||||
@@ -3686,7 +3687,8 @@ pub async fn push<'c, 'd>(
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_super_admin {
|
||||
if email != ERROR_HANDLER_USER_EMAIL
|
||||
if !team_plan_status.premium
|
||||
&& email != ERROR_HANDLER_USER_EMAIL
|
||||
&& email != SCHEDULE_ERROR_HANDLER_USER_EMAIL
|
||||
&& email != SCHEDULE_RECOVERY_HANDLER_USER_EMAIL
|
||||
&& email != "worker@windmill.dev"
|
||||
@@ -3765,43 +3767,53 @@ pub async fn push<'c, 'd>(
|
||||
.flatten()
|
||||
.unwrap_or(1)
|
||||
};
|
||||
if team_plan_status.premium {
|
||||
// team plan is premium but past due, we check if the workspace has exceeded the max tolerated executions
|
||||
if team_plan_status.max_tolerated_executions.is_none()
|
||||
|| workspace_usage > team_plan_status.max_tolerated_executions.unwrap()
|
||||
{
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} team plan is past due and isn't allowed to run any more jobs. Please fix your payment method in the workspace settings."
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
if workspace_usage > MAX_FREE_EXECS
|
||||
&& !matches!(job_payload, JobPayload::Dependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
|
||||
{
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
|
||||
if workspace_usage > MAX_FREE_EXECS
|
||||
&& !matches!(job_payload, JobPayload::Dependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::FlowDependencies { .. })
|
||||
&& !matches!(job_payload, JobPayload::AppDependencies { .. })
|
||||
{
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} has exceeded the free usage limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
let in_queue_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM v2_job_queue WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let in_queue_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM v2_job_queue WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
if in_queue_workspace > MAX_FREE_EXECS as i64 {
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
|
||||
if in_queue_workspace > MAX_FREE_EXECS as i64 {
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} has exceeded the jobs in queue limit of {MAX_FREE_EXECS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
|
||||
let concurrent_runs_workspace = sqlx::query_scalar!(
|
||||
let concurrent_runs_workspace = sqlx::query_scalar!(
|
||||
"SELECT COUNT(id) FROM v2_job_queue WHERE running = true AND workspace_id = $1",
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
workspace_id
|
||||
)
|
||||
.fetch_one(_db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
if concurrent_runs_workspace > MAX_FREE_CONCURRENT_RUNS as i64 {
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
|
||||
)));
|
||||
if concurrent_runs_workspace > MAX_FREE_CONCURRENT_RUNS as i64 {
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Workspace {workspace_id} has exceeded the concurrent runs limit of {MAX_FREE_CONCURRENT_RUNS} that applies outside of premium workspaces."
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,11 +680,12 @@ pub async fn resolve_job_timeout(
|
||||
let mut warn_msg: Option<String> = None;
|
||||
#[cfg(feature = "cloud")]
|
||||
let cloud_premium_workspace = *CLOUD_HOSTED
|
||||
&& windmill_common::workspaces::is_premium_workspace(
|
||||
&& windmill_common::workspaces::get_team_plan_status(
|
||||
_conn.as_sql().expect("cloud cannot use http connection"),
|
||||
_w_id,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.premium;
|
||||
#[cfg(not(feature = "cloud"))]
|
||||
let cloud_premium_workspace = false;
|
||||
|
||||
|
||||
@@ -520,7 +520,7 @@ function get_from_env(name) {{
|
||||
.map(|a| { format!("let {a} = get_from_env(\"{a}\");\n",) })
|
||||
.join(""),
|
||||
if expr.contains("error") && transform_context.contains(&"previous_result".to_string()) {
|
||||
"let error = previous_result.error"
|
||||
"let error = previous_result.error;"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
|
||||
@@ -1027,7 +1027,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
.ok_or_else(|| Error::internal_err(format!("requiring flow to be in the queue")))?;
|
||||
tx.commit().await?;
|
||||
|
||||
if matches!(module_step, Step::PreprocessorStep) {
|
||||
if matches!(module_step, Step::PreprocessorStep) && success {
|
||||
let tag_and_concurrency_key = get_tag_and_concurrency(&flow, db).await;
|
||||
let require_args = tag_and_concurrency_key.as_ref().is_some_and(|x| {
|
||||
x.tag.as_ref().is_some_and(|t| t.contains("$args"))
|
||||
@@ -1144,9 +1144,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
"error while updating args in preprocessing step: {e:#}"
|
||||
))
|
||||
})?;
|
||||
if success {
|
||||
return Ok(UpdateFlowStatusAfterJobCompletion::PreprocessingStep);
|
||||
}
|
||||
return Ok(UpdateFlowStatusAfterJobCompletion::PreprocessingStep);
|
||||
}
|
||||
|
||||
let job_root = flow_job
|
||||
|
||||
+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.536.0";
|
||||
export const VERSION = "v1.538.0";
|
||||
|
||||
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.536.0";
|
||||
export const VERSION = "1.538.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
Generated
+34
-163
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.536.0",
|
||||
"version": "1.538.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.536.0",
|
||||
"version": "1.538.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.60.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~20.2.1",
|
||||
"@codingame/monaco-vscode-editor-api": "~20.2.1",
|
||||
@@ -25,7 +26,7 @@
|
||||
"@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",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.48",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.44",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xyflow/svelte": "^1.0.0",
|
||||
"ag-charts-community": "^9.0.1",
|
||||
@@ -48,7 +49,7 @@
|
||||
"highlight.js": "^11.8.0",
|
||||
"idb": "^8.0.2",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"lucide-svelte": "^0.540.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~20.2.1",
|
||||
"monaco-editor-wrapper": "6.12.0",
|
||||
@@ -56,7 +57,7 @@
|
||||
"monaco-languageclient": "9.11.0",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.87.1",
|
||||
"openai": "^5.16.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.1.0",
|
||||
"panzoom": "^9.4.3",
|
||||
@@ -186,6 +187,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.60.0.tgz",
|
||||
"integrity": "sha512-9zu/TXaUy8BZhXedDtt1wT3H4LOlpKDO1/ftiFpeR3N1PCr3KJFKkxxlQWWt1NNp08xSwUNJ3JNY8yhl8av6eQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.1.tgz",
|
||||
@@ -3640,20 +3650,12 @@
|
||||
"version": "20.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz",
|
||||
"integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
|
||||
@@ -3898,6 +3900,7 @@
|
||||
"version": "0.9.48",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-dnd-action/-/svelte-dnd-action-0.9.48.tgz",
|
||||
"integrity": "sha512-A6pWayH3nOi79DZohTscGj5t2PuNtHlKM5WIAj9WlVDx5pS1A+MsyfuNZi8oCpj2590wFmQl+58G4y4xZqDa1Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3.23.0 || ^5.0.0-next.0"
|
||||
}
|
||||
@@ -3953,17 +3956,6 @@
|
||||
"svelte": "^3.57.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/abstract-leveldown": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
|
||||
@@ -4028,17 +4020,6 @@
|
||||
"ag-grid-community": "31.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -4210,7 +4191,8 @@
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.21",
|
||||
@@ -4802,6 +4784,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
@@ -5426,6 +5409,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -5742,6 +5726,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
@@ -6171,14 +6156,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||
@@ -6405,6 +6382,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
@@ -6415,23 +6393,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@@ -7129,14 +7090,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/idb": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
|
||||
@@ -7895,9 +7848,10 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/lucide-svelte": {
|
||||
"version": "0.399.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.399.0.tgz",
|
||||
"integrity": "sha512-NQ8AxNMKbIJsx7HV//gnAsIY1wJfb3rbXSK2S/ZDjIldvAEdzGngpUT8T8Q8zHYUuii0bavAmVARN8giR4vvpA==",
|
||||
"version": "0.540.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.540.0.tgz",
|
||||
"integrity": "sha512-jedJgKrrsT1B6mAMHGMmk6dIZKhorLq4KO7v+j7hYfeGR0LRe3W+SvwpWF1E6WT2/AY9IAm8KT/G+Nzf+0lW3g==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
}
|
||||
@@ -8784,6 +8738,7 @@
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -8792,6 +8747,7 @@
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
@@ -9141,44 +9097,6 @@
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch-native": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
|
||||
@@ -9363,18 +9281,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.100.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.100.0.tgz",
|
||||
"integrity": "sha512-9soq/wukv3utxcuD7TWFqKdKp0INWdeyhUCvxwrne5KwnxaCp4eHL4GdT/tMFhYolxgNhxFzg5GFwM331Z5CZg==",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"version": "5.16.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-5.16.0.tgz",
|
||||
"integrity": "sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
@@ -9391,19 +9301,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node": {
|
||||
"version": "18.19.101",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.101.tgz",
|
||||
"integrity": "sha512-Ykg7fcE3+cOQlLUv2Ds3zil6DVjriGQaSN/kEpl5HQ3DIGM6W0F2n9+GkWV4bRt7KjLymgzNdTnSKCbFUUJ7Kw==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
@@ -12381,11 +12278,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
@@ -12521,6 +12413,7 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
@@ -12956,33 +12849,11 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/web-worker": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
||||
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wheel": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.536.0",
|
||||
"version": "1.538.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -77,6 +77,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.60.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~20.2.1",
|
||||
"@codingame/monaco-vscode-editor-api": "~20.2.1",
|
||||
@@ -92,7 +93,7 @@
|
||||
"@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",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.48",
|
||||
"@windmill-labs/svelte-dnd-action": "^0.9.44",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xyflow/svelte": "^1.0.0",
|
||||
"ag-charts-community": "^9.0.1",
|
||||
@@ -115,7 +116,7 @@
|
||||
"highlight.js": "^11.8.0",
|
||||
"idb": "^8.0.2",
|
||||
"lru-cache": "^11.1.0",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"lucide-svelte": "^0.540.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~20.2.1",
|
||||
"monaco-editor-wrapper": "6.12.0",
|
||||
@@ -123,7 +124,7 @@
|
||||
"monaco-languageclient": "9.11.0",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.87.1",
|
||||
"openai": "^5.16.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-limit": "^6.1.0",
|
||||
"panzoom": "^9.4.3",
|
||||
@@ -544,4 +545,4 @@
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
|
||||
"fsevents": "^2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare type Item = import('svelte-dnd-action').Item
|
||||
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
|
||||
declare type Item = import('@windmill-labs/svelte-dnd-action').Item
|
||||
declare type DndEvent<ItemType = Item> = import('@windmill-labs/svelte-dnd-action').DndEvent<ItemType>
|
||||
declare namespace svelte.JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
|
||||
@@ -95,14 +95,14 @@
|
||||
<div class="relative">
|
||||
{#if JSON.stringify(value).length > 120}
|
||||
<button
|
||||
class="text-xs absolute top-0 right-4 text-tertiary"
|
||||
class="text-xs absolute top-0 right-8 text-tertiary"
|
||||
onclick={() => {
|
||||
jsonViewerContent = value
|
||||
jsonViewer?.toggleDrawer()
|
||||
}}><Expand size={18} /></button
|
||||
>
|
||||
{/if}
|
||||
<div class="max-h-60 overflow-auto">
|
||||
<div class="max-h-60 overflow-auto" style="scrollbar-gutter: stable">
|
||||
<ObjectViewer collapsed={false} topBrackets={true} pureViewer={true} json={value} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -992,8 +992,11 @@
|
||||
}
|
||||
}
|
||||
bind:args={value}
|
||||
dndType={`nested-${title}`}
|
||||
hiddenArgs={['label', 'kind']}
|
||||
hiddenArgs={[
|
||||
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
|
||||
? 'kind'
|
||||
: 'label'
|
||||
]}
|
||||
on:reorder={(e) => {
|
||||
if (oneOf && oneOf[objIdx]) {
|
||||
const keys = e.detail
|
||||
@@ -1086,20 +1089,14 @@
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
{prettifyHeader}
|
||||
bind:schema={
|
||||
() => ({
|
||||
properties,
|
||||
$schema: '',
|
||||
required: nestedRequired ?? [],
|
||||
type: 'object',
|
||||
order
|
||||
}),
|
||||
(newSchema) => {
|
||||
dispatch('nestedChange')
|
||||
}
|
||||
}
|
||||
schema={{
|
||||
properties,
|
||||
$schema: '',
|
||||
required: nestedRequired ?? [],
|
||||
type: 'object',
|
||||
order
|
||||
}}
|
||||
bind:args={value}
|
||||
dndType={`nested-${title}`}
|
||||
on:reorder={(e) => {
|
||||
const keys = e.detail
|
||||
order = keys
|
||||
|
||||
@@ -13,16 +13,24 @@
|
||||
Tooltip
|
||||
} from 'chart.js'
|
||||
import type { CompletedJob, ExtendedJobs } from '$lib/gen'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getDbClockNow } from '$lib/forLater'
|
||||
import { Line } from '$lib/components/chartjs-wrappers/chartJs'
|
||||
|
||||
export let extendedJobs: ExtendedJobs | undefined = undefined
|
||||
export let maxIsNow: boolean = false
|
||||
export let minTimeSet: string | undefined = undefined
|
||||
export let maxTimeSet: string | undefined = undefined
|
||||
interface Props {
|
||||
extendedJobs?: ExtendedJobs | undefined
|
||||
maxIsNow?: boolean
|
||||
minTimeSet?: string | undefined
|
||||
maxTimeSet?: string | undefined
|
||||
onZoom: (zoom: { min: Date; max: Date }) => void
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let {
|
||||
extendedJobs = undefined,
|
||||
maxIsNow = false,
|
||||
minTimeSet = undefined,
|
||||
maxTimeSet = undefined,
|
||||
onZoom
|
||||
}: Props = $props()
|
||||
|
||||
function calculateTimeSeries(extendedJobs: ExtendedJobs): AggregatedInterval[] {
|
||||
const timeline = new Map<number, { count: number; id_started: string[]; id_ended: string[] }>()
|
||||
@@ -57,7 +65,7 @@
|
||||
}
|
||||
const s = timeline.get(startTime)!
|
||||
s.count += 1
|
||||
s.id_started.push('unknoww')
|
||||
s.id_started.push('unknown')
|
||||
if (j.duration_ms != undefined) {
|
||||
const jc = j as CompletedJob
|
||||
const endTime = startTime + jc.duration_ms
|
||||
@@ -103,9 +111,6 @@
|
||||
}
|
||||
type AggregatedInterval = { time: Date; count: number; msg?: string }
|
||||
|
||||
let intervals: AggregatedInterval[] | undefined = undefined
|
||||
$: intervals = extendedJobs ? calculateTimeSeries(extendedJobs) : undefined
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
@@ -118,31 +123,12 @@
|
||||
TimeScale
|
||||
)
|
||||
|
||||
$: data = {
|
||||
datasets: [
|
||||
{
|
||||
borderColor: '#4ade80',
|
||||
backgroundColor: '#f8717100',
|
||||
pointRadius: 0,
|
||||
label: 'running',
|
||||
showLine: true,
|
||||
stepped: true,
|
||||
data:
|
||||
intervals?.map((job) => ({
|
||||
x: job.time as any,
|
||||
y: job.count,
|
||||
id: job.msg
|
||||
})) ?? []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const zoomOptions = {
|
||||
pan: {
|
||||
enabled: true,
|
||||
modifierKey: 'ctrl' as 'ctrl',
|
||||
onPanComplete: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
onZoom({
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
@@ -154,17 +140,13 @@
|
||||
},
|
||||
mode: 'x' as 'x',
|
||||
onZoom: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
onZoom({
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
let minTime = addSeconds(new Date(), -300)
|
||||
let maxTime = getDbClockNow()
|
||||
|
||||
$: computeMinMaxTime(intervals, minTimeSet, maxTimeSet)
|
||||
|
||||
function minJobTime(intervals: AggregatedInterval[]): Date {
|
||||
return intervals[0].time
|
||||
@@ -181,15 +163,13 @@
|
||||
let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined
|
||||
let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined
|
||||
if (minTimeSetDate && maxTimeSetDate) {
|
||||
minTime = minTimeSetDate
|
||||
maxTime = maxTimeSetDate
|
||||
return
|
||||
return { min: minTimeSetDate, max: maxTimeSetDate }
|
||||
}
|
||||
|
||||
if (intervals == undefined || intervals?.length == 0) {
|
||||
minTime = minTimeSetDate ?? addSeconds(new Date(), -300)
|
||||
maxTime = maxTimeSetDate ?? getDbClockNow()
|
||||
return
|
||||
const minTime = minTimeSetDate ?? addSeconds(new Date(), -300)
|
||||
const maxTime = maxTimeSetDate ?? getDbClockNow()
|
||||
return { min: minTime, max: maxTime }
|
||||
}
|
||||
|
||||
const maxJob = maxIsNow ? getDbClockNow() : maxJobTime(intervals)
|
||||
@@ -197,12 +177,11 @@
|
||||
|
||||
const diff = (maxJob.getTime() - minJob.getTime()) / 20000
|
||||
|
||||
minTime = minTimeSetDate ?? addSeconds(minJob, -diff)
|
||||
if (maxIsNow) {
|
||||
maxTime = maxTimeSetDate ?? maxJob
|
||||
} else {
|
||||
maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff)
|
||||
}
|
||||
const minTime = minTimeSetDate ?? addSeconds(minJob, -diff)
|
||||
const maxTime = maxIsNow
|
||||
? (maxTimeSetDate ?? maxJob)
|
||||
: (maxTimeSetDate ?? addSeconds(maxJob, diff))
|
||||
return { min: minTime, max: maxTime }
|
||||
}
|
||||
|
||||
function addSeconds(date: Date, seconds: number): Date {
|
||||
@@ -210,7 +189,30 @@
|
||||
return date
|
||||
}
|
||||
|
||||
$: options = {
|
||||
const intervals = $derived(extendedJobs ? calculateTimeSeries(extendedJobs) : undefined)
|
||||
|
||||
let data = $derived({
|
||||
datasets: [
|
||||
{
|
||||
borderColor: '#4ade80',
|
||||
backgroundColor: '#f8717100',
|
||||
pointRadius: 0,
|
||||
label: 'running',
|
||||
showLine: true,
|
||||
stepped: true,
|
||||
data:
|
||||
intervals?.map((job) => ({
|
||||
x: job.time as any,
|
||||
y: job.count,
|
||||
id: job.msg
|
||||
})) ?? []
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const minMaxTimes = $derived(computeMinMaxTime(intervals, minTimeSet, maxTimeSet))
|
||||
|
||||
let options = $derived({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
@@ -232,8 +234,8 @@
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
min: minTime,
|
||||
max: maxTime
|
||||
min: minMaxTimes.min,
|
||||
max: minMaxTimes.max
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
@@ -254,7 +256,7 @@
|
||||
intersect: false,
|
||||
mode: 'index'
|
||||
}
|
||||
} as any
|
||||
} as any)
|
||||
</script>
|
||||
|
||||
<div class="relative max-h-40">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { Item } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
items?: Item[]
|
||||
extraLabel?: import('svelte').Snippet
|
||||
selected: string
|
||||
selectedDisplayName?: string
|
||||
btnClasses?: string
|
||||
}
|
||||
|
||||
let { items = [], extraLabel, selected, selectedDisplayName, btnClasses }: Props = $props()
|
||||
|
||||
const filteredItems = $derived(items.filter((item) => item.id !== selected))
|
||||
</script>
|
||||
|
||||
<DropdownV2 items={filteredItems}>
|
||||
{#snippet buttonReplacement()}
|
||||
<div
|
||||
class={twMerge(
|
||||
'p-2 h-8 flex flex-row items-center gap-2 border hover:bg-surface-hover cursor-pointer rounded-md',
|
||||
btnClasses
|
||||
)}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-1 pr-2 justify-between w-full">
|
||||
<span class="text-xs whitespace-nowrap">
|
||||
{selectedDisplayName ?? items.find((item) => item.id === selected)?.displayName ?? ''}
|
||||
</span>
|
||||
|
||||
{@render extraLabel?.()}
|
||||
</div>
|
||||
<ChevronDown size={12} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
@@ -47,7 +47,7 @@
|
||||
aiDescription={item.displayName}
|
||||
>
|
||||
{#if item.icon}
|
||||
<item.icon size={14} color={item.iconColor} />
|
||||
<item.icon size={14} color={item.iconColor} class="shrink-0" />
|
||||
{/if}
|
||||
<p title={item.displayName} class="truncate grow min-w-0 whitespace-nowrap text-left">
|
||||
{item.displayName}
|
||||
|
||||
@@ -14,13 +14,18 @@
|
||||
import FlowPropertyEditor from './schema/FlowPropertyEditor.svelte'
|
||||
import PropertyEditor from './schema/PropertyEditor.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import { createEventDispatcher, tick, untrack } from 'svelte'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { DynamicSelect, emptyString } from '$lib/utils'
|
||||
import {
|
||||
DynamicSelect,
|
||||
emptyString,
|
||||
generateRandomString,
|
||||
readFieldsRecursively
|
||||
} from '$lib/utils'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
@@ -48,7 +53,6 @@
|
||||
isAppInput?: boolean
|
||||
displayWebhookWarning?: boolean
|
||||
onlyMaskPassword?: boolean
|
||||
dndType?: string | undefined
|
||||
editTab:
|
||||
| 'inputEditor'
|
||||
| 'history'
|
||||
@@ -75,6 +79,8 @@
|
||||
addProperty?: import('svelte').Snippet
|
||||
runButton?: import('svelte').Snippet
|
||||
extraTab?: import('svelte').Snippet
|
||||
schemaFormClassName?: string
|
||||
onChange?: (args: Record<string, any>) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -91,7 +97,6 @@
|
||||
isAppInput = false,
|
||||
displayWebhookWarning = false,
|
||||
onlyMaskPassword = false,
|
||||
dndType = undefined,
|
||||
editTab,
|
||||
previewSchema = undefined,
|
||||
editPanelInitialSize = undefined,
|
||||
@@ -110,7 +115,9 @@
|
||||
openEditTab,
|
||||
addProperty,
|
||||
runButton,
|
||||
extraTab
|
||||
extraTab,
|
||||
schemaFormClassName = undefined,
|
||||
onChange = undefined
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -125,6 +132,13 @@
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (onChange) {
|
||||
readFieldsRecursively(args)
|
||||
onChange(args ?? {})
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (schema && dynSelectCode !== undefined && dynSelectLang !== undefined) {
|
||||
if (dynSelectCode && dynSelectCode.trim()) {
|
||||
@@ -154,9 +168,10 @@
|
||||
let variableEditor: VariableEditor | undefined = $state(undefined)
|
||||
|
||||
let keys: string[] = $state(
|
||||
Array.isArray(schema?.order)
|
||||
(Array.isArray(schema?.order)
|
||||
? [...schema.order]
|
||||
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
|
||||
).filter((x) => !hiddenArgs?.includes(x))
|
||||
)
|
||||
|
||||
function alignOrderWithProperties(schema: {
|
||||
@@ -187,37 +202,30 @@
|
||||
return hasChanged
|
||||
}
|
||||
function onSchemaChange() {
|
||||
let editSchema = false
|
||||
if (alignOrderWithProperties(schema)) {
|
||||
console.log('alignOrderWithProperties', JSON.stringify(schema, null, 2))
|
||||
editSchema = true
|
||||
// console.log('alignOrderWithProperties', JSON.stringify(schema, null, 2))
|
||||
}
|
||||
let lkeys = schema?.order ?? Object.keys(schema?.properties ?? {})
|
||||
let lkeys = (schema?.order ?? Object.keys(schema?.properties ?? {})).filter(
|
||||
(x) => !hiddenArgs?.includes(x)
|
||||
)
|
||||
if (schema?.properties && !deepEqual(lkeys, keys)) {
|
||||
keys = [...lkeys]
|
||||
editSchema = true
|
||||
if (opened == undefined) {
|
||||
opened = keys[0]
|
||||
}
|
||||
}
|
||||
if (editSchema) {
|
||||
schema = schema
|
||||
}
|
||||
}
|
||||
|
||||
let opened: string | undefined = $state(untrack(() => keys[0]))
|
||||
|
||||
function computeSelected(property: any) {
|
||||
if (!opened) return ''
|
||||
return property.type !== 'object'
|
||||
? property.type
|
||||
: property.format === 'resource-s3_object'
|
||||
? 'S3'
|
||||
: property.format?.startsWith('dynselect-')
|
||||
? 'dynselect'
|
||||
: property.oneOf && property.oneOf.length >= 2
|
||||
? 'oneOf'
|
||||
: 'object'
|
||||
if (property.type !== 'object') return property.type
|
||||
if (property.format === 'resource-s3_object') return 'S3'
|
||||
if (property.format?.startsWith('dynselect-')) return 'dynselect'
|
||||
if (property.oneOf && property.oneOf.length >= 2) return 'oneOf'
|
||||
if (property.format?.startsWith('resource-')) return 'resource'
|
||||
return 'object'
|
||||
}
|
||||
|
||||
export function openField(key: string) {
|
||||
@@ -248,30 +256,31 @@
|
||||
// clear the input
|
||||
el.value = oldName
|
||||
} else {
|
||||
let newSchema = $state.snapshot(schema)
|
||||
if (args) {
|
||||
args[newName] = args[oldName]
|
||||
delete args[oldName]
|
||||
}
|
||||
|
||||
schema.properties[newName] = schema.properties[oldName]
|
||||
delete schema.properties[oldName]
|
||||
newSchema.properties[newName] = newSchema.properties[oldName]
|
||||
delete newSchema.properties[oldName]
|
||||
|
||||
if (schema.required?.includes(oldName)) {
|
||||
schema.required = schema.required?.map((x) => (x === oldName ? newName : x))
|
||||
if (newSchema.required?.includes(oldName)) {
|
||||
newSchema.required = newSchema.required?.map((x) => (x === oldName ? newName : x))
|
||||
}
|
||||
|
||||
// Replace the old name with the new name in the order array
|
||||
if (schema.order) {
|
||||
const index = schema.order.indexOf(oldName)
|
||||
if (newSchema.order) {
|
||||
const index = newSchema.order.indexOf(oldName)
|
||||
if (index !== -1) {
|
||||
schema.order[index] = newName
|
||||
newSchema.order[index] = newName
|
||||
}
|
||||
}
|
||||
|
||||
opened = newName
|
||||
|
||||
schema = $state.snapshot(schema)
|
||||
dispatch('change', schema)
|
||||
schema = newSchema
|
||||
|
||||
sendUserToast('Argument renamed')
|
||||
}
|
||||
}
|
||||
@@ -347,10 +356,11 @@
|
||||
['Number', 'number'],
|
||||
['Integer', 'integer'],
|
||||
['Object', 'object'],
|
||||
['Resource', 'resource'],
|
||||
['OneOf', 'oneOf'],
|
||||
['Array', 'array'],
|
||||
['Boolean', 'boolean'],
|
||||
['S3 Object', 'S3']
|
||||
['S3', 'S3']
|
||||
]
|
||||
if (showDynSelectOpt) {
|
||||
typeOptions.push(['DynSelect', 'dynselect'])
|
||||
@@ -368,6 +378,8 @@
|
||||
const code = generateFn(functionName)
|
||||
dynSelectCode = dynSelectCode ? dynSelectCode.concat(code) : code
|
||||
}
|
||||
|
||||
let dndType = $state(generateRandomString())
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
@@ -404,15 +416,15 @@
|
||||
class="min-h-0 overflow-y-auto grow rounded-md {runButton ? 'flex flex-col gap-2' : ''}"
|
||||
>
|
||||
<SchemaFormDnd
|
||||
nestedClasses={'flex flex-col gap-1'}
|
||||
{dndType}
|
||||
nestedClasses={'flex flex-col gap-1 ' + (schemaFormClassName ?? '')}
|
||||
bind:schema={
|
||||
() => (previewSchema ? previewSchema : schema),
|
||||
(newSchema) => {
|
||||
schema = newSchema
|
||||
tick().then(() => dispatch('change', schema))
|
||||
}
|
||||
}
|
||||
{dndType}
|
||||
{hiddenArgs}
|
||||
{disableDnd}
|
||||
{onlyMaskPassword}
|
||||
bind:args
|
||||
@@ -420,11 +432,16 @@
|
||||
opened = e.detail
|
||||
}}
|
||||
on:reorder={(e) => {
|
||||
let order = e.detail
|
||||
let newProperties = {}
|
||||
for (let key of order) {
|
||||
newProperties[key] = schema.properties[key]
|
||||
}
|
||||
schema = {
|
||||
...schema,
|
||||
properties: newProperties,
|
||||
order: e.detail
|
||||
}
|
||||
tick().then(() => dispatch('change', schema))
|
||||
}}
|
||||
helperScript={{
|
||||
type: 'inline',
|
||||
@@ -436,9 +453,6 @@
|
||||
{diff}
|
||||
on:acceptChange
|
||||
on:rejectChange
|
||||
on:nestedChange={() => {
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
{shouldDispatchChanges}
|
||||
bind:isValid
|
||||
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
|
||||
@@ -446,8 +460,8 @@
|
||||
|
||||
{@render runButton?.()}
|
||||
|
||||
<div class="h-full">
|
||||
{#if dynSelectFunctions.length > 0}
|
||||
{#if dynSelectFunctions.length > 0}
|
||||
<div class="grow">
|
||||
<Section
|
||||
label="Dynamic select functions"
|
||||
collapsable={true}
|
||||
@@ -494,8 +508,8 @@
|
||||
{/key}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
@@ -514,7 +528,7 @@
|
||||
{#if jsonEnabled && customUi?.jsonOnly != true}
|
||||
<div class="w-full p-3 flex gap-4 justify-end items-center">
|
||||
{#if addPropertyInEditorTab}
|
||||
<AddPropertyV2 bind:schema on:change>
|
||||
<AddPropertyV2 bind:schema>
|
||||
{#snippet trigger()}
|
||||
<Button color="light" size="xs" iconOnly startIcon={{ icon: Plus }} />
|
||||
{/snippet}
|
||||
@@ -531,9 +545,13 @@
|
||||
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
} else {
|
||||
schema = JSON.parse(schemaString)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -629,7 +647,7 @@
|
||||
</div>
|
||||
{#if opened === argName}
|
||||
<div class="p-4 border-t">
|
||||
{#if !hiddenArgs.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
{#if Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
<PropertyEditor
|
||||
bind:description={schema.properties[argName].description}
|
||||
@@ -650,10 +668,6 @@
|
||||
bind:order={schema.properties[argName].order}
|
||||
{isFlowInput}
|
||||
{isAppInput}
|
||||
on:change={() => {
|
||||
schema = $state.snapshot(schema)
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
>
|
||||
{#snippet typeeditor()}
|
||||
{#if isFlowInput || isAppInput}
|
||||
@@ -664,6 +678,7 @@
|
||||
bind:selected={
|
||||
() => computeSelected(schema.properties[opened ?? '']),
|
||||
(v) => {
|
||||
const isResource = v == 'resource'
|
||||
const isS3 = v == 'S3'
|
||||
const isOneOf = v == 'oneOf'
|
||||
const isDynSelect = v == 'dynselect'
|
||||
@@ -684,13 +699,18 @@
|
||||
nullable: undefined,
|
||||
required: undefined
|
||||
}
|
||||
|
||||
if (isS3) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
format: 'resource-s3_object'
|
||||
}
|
||||
} else if (isResource) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
format: 'resource-'
|
||||
}
|
||||
} else if (isDynSelect) {
|
||||
const functionName = argName.replace(/\s+/g, '_')
|
||||
schema.properties[argName] = {
|
||||
@@ -742,13 +762,9 @@
|
||||
type: v
|
||||
}
|
||||
}
|
||||
schema.properties = schema.properties
|
||||
}
|
||||
}
|
||||
on:selected={(e) => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
dispatch('schemaChange')
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
{#each typeOptions as x}
|
||||
@@ -762,6 +778,9 @@
|
||||
|
||||
{#if isFlowInput || isAppInput}
|
||||
<FlowPropertyEditor
|
||||
onDrawerClose={() => {
|
||||
dndType = generateRandomString()
|
||||
}}
|
||||
bind:defaultValue={schema.properties[argName].default}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
@@ -792,12 +811,6 @@
|
||||
(x) => x !== argName
|
||||
)
|
||||
}
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
on:schemaChange={(e) => {
|
||||
schema = $state.snapshot(schema)
|
||||
dispatch('change', schema)
|
||||
dispatch('schemaChange')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -822,7 +835,6 @@
|
||||
on:change={() => {
|
||||
try {
|
||||
schema = JSON.parse(schemaString)
|
||||
dispatch('change', schema)
|
||||
error = ''
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
|
||||
@@ -150,11 +150,12 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { formatResourceTypes } from './copilot/chat/script/core'
|
||||
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
|
||||
import { editorPositionMap, readFieldsRecursively } from '$lib/utils'
|
||||
import { editorPositionMap } from '$lib/utils'
|
||||
import { extToLang, langToExt } from '$lib/editorLangUtils'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
import type { Selection } from 'monaco-editor'
|
||||
import { getDbSchemas } from './apps/components/display/dbtable/utils'
|
||||
import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers'
|
||||
// import EditorTheme from './EditorTheme.svelte'
|
||||
|
||||
let divEl: HTMLDivElement | null = $state(null)
|
||||
@@ -185,6 +186,7 @@
|
||||
key?: string | undefined
|
||||
class?: string | undefined
|
||||
moduleId?: string
|
||||
enablePreprocessorSnippet?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -211,7 +213,8 @@
|
||||
loadAsync = false,
|
||||
key = undefined,
|
||||
class: clazz = undefined,
|
||||
moduleId = undefined
|
||||
moduleId = undefined,
|
||||
enablePreprocessorSnippet = false
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -544,9 +547,7 @@
|
||||
|
||||
let sqlSchemaCompletor: IDisposable | undefined = undefined
|
||||
|
||||
async function updateSchema() {
|
||||
const newSchemaRes = lang === 'graphql' ? args?.api : args?.database
|
||||
|
||||
async function updateSchema(newSchemaRes: string | undefined) {
|
||||
if (typeof newSchemaRes === 'string') {
|
||||
const resourcePath = newSchemaRes.replace('$res:', '')
|
||||
dbSchema = $dbSchemas[resourcePath]
|
||||
@@ -667,6 +668,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
let preprocessorCompletor: IDisposable | undefined = undefined
|
||||
function addPreprocessorCompletions(lang: 'typescript' | 'python') {
|
||||
if (preprocessorCompletor) {
|
||||
preprocessorCompletor.dispose()
|
||||
}
|
||||
const preprocessorCode =
|
||||
lang === 'typescript' ? TS_PREPROCESSOR_MODULE_CODE : PYTHON_PREPROCESSOR_MODULE_CODE
|
||||
preprocessorCompletor = languages.registerCompletionItemProvider(lang, {
|
||||
provideCompletionItems: function (model, position) {
|
||||
const word = model.getWordUntilPosition(position)
|
||||
|
||||
if (word.word.length >= 3 && 'preprocessor'.startsWith(word.word)) {
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
}
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'preprocessor (windmill)',
|
||||
kind: languages.CompletionItemKind.Function,
|
||||
insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
insertText: preprocessorCode,
|
||||
range,
|
||||
additionalTextEdits: [
|
||||
{
|
||||
range: {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: 0,
|
||||
endColumn: word.startColumn
|
||||
},
|
||||
text: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: []
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let reviewingChanges = $state(writable(false))
|
||||
let aiChatEditorHandler: AIChatEditorHandler | undefined = $state(undefined)
|
||||
|
||||
@@ -1570,6 +1620,7 @@
|
||||
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
|
||||
autocompletor && autocompletor.dispose()
|
||||
sqlTypeCompletor && sqlTypeCompletor.dispose()
|
||||
preprocessorCompletor && preprocessorCompletor.dispose()
|
||||
timeoutModel && clearTimeout(timeoutModel)
|
||||
loadTimeout && clearTimeout(loadTimeout)
|
||||
aiChatEditorHandler?.clear()
|
||||
@@ -1621,10 +1672,20 @@
|
||||
? untrack(() => addSqlTypeCompletions())
|
||||
: sqlTypeCompletor?.dispose()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
console.log('updating schema', lang, $dbSchemas)
|
||||
readFieldsRecursively(args)
|
||||
lang && $dbSchemas && untrack(() => updateSchema())
|
||||
initialized && (lang === 'typescript' || lang === 'python') && enablePreprocessorSnippet
|
||||
? untrack(() => addPreprocessorCompletions(lang as 'typescript' | 'python'))
|
||||
: preprocessorCompletor?.dispose()
|
||||
})
|
||||
|
||||
let lastArg = undefined
|
||||
$effect(() => {
|
||||
let newArg = lang === 'graphql' ? args?.api : args?.database
|
||||
if (newArg !== lastArg) {
|
||||
lastArg = newArg
|
||||
$dbSchemas && untrack(() => updateSchema(newArg))
|
||||
}
|
||||
})
|
||||
$effect(() => {
|
||||
console.log('updating db schema completions', dbSchema, lang)
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
AI_TOOL_MESSAGE_PREFIX,
|
||||
getToolCallId
|
||||
} from './graph/renderers/nodes/AIToolNode.svelte'
|
||||
import JobAssetsViewer from './assets/JobAssetsViewer.svelte'
|
||||
|
||||
let {
|
||||
flowStateStore,
|
||||
@@ -1048,7 +1049,11 @@
|
||||
}
|
||||
}
|
||||
})
|
||||
let selected = $derived(isListJob ? 'sequence' : 'graph') as 'sequence' | 'graph' | 'logs'
|
||||
let selected = $derived(isListJob ? 'sequence' : 'graph') as
|
||||
| 'sequence'
|
||||
| 'graph'
|
||||
| 'logs'
|
||||
| 'assets'
|
||||
|
||||
let animateLogsTab = $state(false)
|
||||
|
||||
@@ -1076,6 +1081,23 @@
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// Set all tabs content to the same height to prevent layout jumps
|
||||
let tabsHeight = $state({
|
||||
sequenceHeight: 0,
|
||||
logsHeight: 0,
|
||||
assetsHeight: 0,
|
||||
graphHeight: 0
|
||||
})
|
||||
|
||||
let minTabHeight = $derived(
|
||||
Math.max(
|
||||
tabsHeight.sequenceHeight,
|
||||
tabsHeight.logsHeight,
|
||||
tabsHeight.assetsHeight,
|
||||
tabsHeight.graphHeight
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
<JobLoader workspaceOverride={workspaceId} {noLogs} noCode bind:this={jobLoader} />
|
||||
@@ -1154,12 +1176,17 @@
|
||||
: ''}><span class="font-semibold">Logs</span></Tab
|
||||
>
|
||||
<Tab value="sequence"><span class="font-semibold">Details</span></Tab>
|
||||
<Tab value="assets"><span class="font-semibold">Assets</span></Tab>
|
||||
</Tabs>
|
||||
{:else}
|
||||
<div class="h-[30px]"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="{selected != 'sequence' ? 'hidden' : ''} max-w-7xl mx-auto">
|
||||
<div
|
||||
class="{selected != 'sequence' ? 'hidden' : ''} max-w-7xl mx-auto"
|
||||
bind:clientHeight={tabsHeight.sequenceHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
{#if isListJob}
|
||||
{@const sliceFrom =
|
||||
globalIterationBounds[buildSubflowKey(flowJobIds?.moduleId ?? '', prefix)]
|
||||
@@ -1508,7 +1535,11 @@
|
||||
<div class="p-2 text-tertiary text-sm italic">Empty flow</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="{selected != 'logs' ? 'hidden' : ''} mx-auto h-[800px]">
|
||||
<div
|
||||
class="{selected != 'logs' ? 'hidden' : ''} mx-auto"
|
||||
bind:clientHeight={tabsHeight.logsHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
<FlowLogViewerWrapper
|
||||
{job}
|
||||
{localModuleStates}
|
||||
@@ -1517,10 +1548,23 @@
|
||||
{onSelectedIteration}
|
||||
/>
|
||||
</div>
|
||||
{#if selected == 'assets' && render}
|
||||
<div
|
||||
class="p-2"
|
||||
bind:clientHeight={tabsHeight.assetsHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
<JobAssetsViewer {job} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if render}
|
||||
{#if job.raw_flow && !isListJob}
|
||||
<div class="{selected != 'graph' ? 'hidden' : ''} grow mt-4">
|
||||
<div
|
||||
class="{selected != 'graph' ? 'hidden' : ''} grow mt-4"
|
||||
bind:clientHeight={tabsHeight.graphHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
<div class="grid grid-cols-3 border h-full" bind:clientHeight={wrapperHeight}>
|
||||
<div class="col-span-2 bg-surface-secondary">
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -85,7 +85,7 @@ ${Object.entries(args)
|
||||
{#each Object.entries(args).sort((a, b) => a[0].localeCompare(b[0])) as [arg, value]}
|
||||
<Row>
|
||||
<Cell first>{arg}</Cell>
|
||||
<Cell last><ArgInfo {value} /></Cell>
|
||||
<Cell><ArgInfo {value} /></Cell>
|
||||
</Row>
|
||||
{/each}
|
||||
{:else if args}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ResourceTypePicker from './ResourceTypePicker.svelte'
|
||||
|
||||
export let format: string | undefined
|
||||
|
||||
let resource: string | undefined = format?.startsWith('resource-')
|
||||
? format.substring('resource-'.length)
|
||||
: undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<ResourceTypePicker
|
||||
on:click={(e) => {
|
||||
resource = e.detail
|
||||
format = resource != undefined ? `resource-${resource}` : undefined
|
||||
dispatch('change', format)
|
||||
}}
|
||||
value={resource}
|
||||
nonePickable={true}
|
||||
/>
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module">
|
||||
<script module>
|
||||
import { tick } from 'svelte'
|
||||
|
||||
export function portal(el, options) {
|
||||
@@ -50,13 +50,17 @@
|
||||
|
||||
<script>
|
||||
/**
|
||||
* DOM Element or CSS Selector
|
||||
* @type { HTMLElement|string}
|
||||
* @typedef {Object} Props
|
||||
* @property { HTMLElement|string} [target] - DOM Element or CSS Selector
|
||||
* @property {any} [name]
|
||||
* @property {string|undefined} [class]
|
||||
* @property {import('svelte').Snippet} [children]
|
||||
*/
|
||||
export let target = 'body'
|
||||
export let name = undefined
|
||||
|
||||
/** @type {Props} */
|
||||
let { target = 'body', name = undefined, class: clazz = undefined, children } = $props()
|
||||
</script>
|
||||
|
||||
<div use:portal={{ target, name }} hidden class={$$props.class}>
|
||||
<slot />
|
||||
<div use:portal={{ target, name }} hidden class={clazz}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import ResourceTypePicker from './ResourceTypePicker.svelte'
|
||||
|
||||
interface Props {
|
||||
format: string | undefined
|
||||
}
|
||||
|
||||
let { format = $bindable() }: Props = $props()
|
||||
|
||||
let resource: string | undefined = $state(
|
||||
format?.startsWith('resource-') ? format.substring('resource-'.length) : undefined
|
||||
)
|
||||
</script>
|
||||
|
||||
<ResourceTypePicker
|
||||
on:click={(e) => {
|
||||
resource = e.detail
|
||||
format = `resource-${resource ?? ''}`
|
||||
}}
|
||||
value={resource}
|
||||
nonePickable={true}
|
||||
/>
|
||||
@@ -5,8 +5,6 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import { Button, ClearableInput } from './common'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import Label from './Label.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
@@ -46,84 +44,61 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<Label label="Resource type" class="w-full col-span-2">
|
||||
<Label label="Resource type" class="w-full mb-4">
|
||||
{#snippet header()}
|
||||
<Tooltip light small>Select a resource type to narrow down the object type.</Tooltip>
|
||||
{/snippet}
|
||||
|
||||
{#snippet action()}
|
||||
<div class="flex flex-row gap-1">
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
on:click={() => onClick(undefined)}
|
||||
disabled={notPickable || value === undefined}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
strategy: 'fixed',
|
||||
placement: 'left-end',
|
||||
middleware: [offset(8), flip(), shift()]
|
||||
}}
|
||||
contentClasses="flex flex-col gap-2 h-full p-4 max-h-[40vh] w-[500px]"
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button nonCaptureEvent size="xs" color="dark">Select resource type</Button>
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<ClearableInput bind:value={search} placeholder="Search resource..." />
|
||||
|
||||
<div class="overflow-y-scroll h-full">
|
||||
<div
|
||||
class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center overflow-x-hidden"
|
||||
>
|
||||
{#if nonePickable && search === ''}
|
||||
{@const isPicked = value === undefined}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => {
|
||||
onClick(undefined)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
None
|
||||
</Button>
|
||||
{/if}
|
||||
{#each filteredResources as r}
|
||||
{@const isPicked = value === r}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => {
|
||||
onClick(r)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={r} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
{#if filteredResources.length === 0 && search !== ''}
|
||||
<div class="text-tertiary text-sm">No resources found</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
<div class="flex flex-row items-center w-full justify-between">
|
||||
<Badge color={!value ? 'gray' : 'blue'}>
|
||||
{value ?? 'None'}
|
||||
</Badge>
|
||||
</div>
|
||||
{/snippet}
|
||||
<div class="flex flex-row items-center w-full justify-between">
|
||||
<Badge color={!value ? 'gray' : 'blue'}>
|
||||
{value ?? 'None'}
|
||||
</Badge>
|
||||
|
||||
<div class="mt-2">
|
||||
<ClearableInput bind:value={search} placeholder="Search resource..." />
|
||||
|
||||
<div class="overflow-y-scroll max-h-[330px] h-full mt-1">
|
||||
<div
|
||||
class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center overflow-x-hidden"
|
||||
>
|
||||
{#if nonePickable && search === ''}
|
||||
{@const isPicked = value === undefined}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => {
|
||||
onClick(undefined)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
None
|
||||
</Button>
|
||||
{/if}
|
||||
{#each filteredResources as r}
|
||||
{@const isPicked = value === r}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'light'}
|
||||
btnClasses={isPicked ? '!border-2' : 'm-[1px]'}
|
||||
disabled={notPickable}
|
||||
on:click={() => {
|
||||
onClick(r)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={r} after={true} width="20px" height="20px" />
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
{#if filteredResources.length === 0 && search !== ''}
|
||||
<div class="text-tertiary text-sm">No resources found</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
@@ -15,28 +15,42 @@
|
||||
LogarithmicScale
|
||||
} from 'chart.js'
|
||||
import type { CompletedJob } from '$lib/gen'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getDbClockNow } from '$lib/forLater'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { Scatter } from '$lib/components/chartjs-wrappers/chartJs'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
|
||||
export let jobs: CompletedJob[] | undefined = []
|
||||
export let maxIsNow: boolean = false
|
||||
export let minTimeSet: string | undefined = undefined
|
||||
export let maxTimeSet: string | undefined = undefined
|
||||
export let selectedIds: string[] = []
|
||||
export let canSelect: boolean = true
|
||||
export let lastFetchWentToEnd: boolean = false
|
||||
interface Props {
|
||||
jobs?: CompletedJob[] | undefined
|
||||
maxIsNow?: boolean
|
||||
minTimeSet?: string | undefined
|
||||
maxTimeSet?: string | undefined
|
||||
selectedIds?: string[]
|
||||
canSelect?: boolean
|
||||
lastFetchWentToEnd?: boolean
|
||||
onPointClicked: (ids: string[]) => void
|
||||
onLoadExtra: () => void
|
||||
onZoom: (zoom: { min: Date; max: Date }) => void
|
||||
}
|
||||
|
||||
let {
|
||||
jobs = [],
|
||||
maxIsNow = false,
|
||||
minTimeSet = undefined,
|
||||
maxTimeSet = undefined,
|
||||
selectedIds = $bindable([]),
|
||||
canSelect = true,
|
||||
lastFetchWentToEnd = false,
|
||||
onPointClicked,
|
||||
onLoadExtra,
|
||||
onZoom
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const SUCCESS_COLOR = '#4ade80'
|
||||
// const SUCCESS_COLOR_TRANSPARENT = '#c9b638'
|
||||
const SUCCESS_COLOR_TRANSPARENT = mergeColors(SUCCESS_COLOR, getBackgorundColor(), 0.8)
|
||||
const SUCCESS_COLOR_TRANSPARENT = $derived(mergeColors(SUCCESS_COLOR, getBackgroundColor(), 0.8))
|
||||
const FAIL_COLOR = '#f87171'
|
||||
const FAIL_COLOR_TRANSPARENT = mergeColors(FAIL_COLOR, getBackgorundColor(), 0.8)
|
||||
|
||||
$: success = jobs?.filter((x) => x.success)
|
||||
$: failed = jobs?.filter((x) => !x.success)
|
||||
const FAIL_COLOR_TRANSPARENT = $derived(mergeColors(FAIL_COLOR, getBackgroundColor(), 0.8))
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
@@ -51,43 +65,12 @@
|
||||
TimeScale
|
||||
)
|
||||
|
||||
$: data = {
|
||||
datasets: [
|
||||
{
|
||||
borderColor: 'rgba(99,0,125, 0)',
|
||||
backgroundColor: FAIL_COLOR as string | string[],
|
||||
radius: 3,
|
||||
label: 'Failed',
|
||||
data:
|
||||
failed?.map((job) => ({
|
||||
x: job.started_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
})) ?? []
|
||||
},
|
||||
{
|
||||
borderColor: 'rgba(99,0,125, 0)',
|
||||
backgroundColor: SUCCESS_COLOR as string | string[],
|
||||
radius: 3,
|
||||
label: 'Successful',
|
||||
data:
|
||||
success?.map((job) => ({
|
||||
x: job.started_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
})) ?? []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const zoomOptions = {
|
||||
pan: {
|
||||
enabled: true,
|
||||
modifierKey: 'ctrl' as 'ctrl',
|
||||
onPanComplete: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
onZoom({
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
@@ -99,7 +82,7 @@
|
||||
},
|
||||
mode: 'x' as 'x',
|
||||
onZoom: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
onZoom({
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
@@ -107,15 +90,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isDark(): boolean {
|
||||
return document.documentElement.classList.contains('dark')
|
||||
}
|
||||
let darkMode = $state(false)
|
||||
$effect(() => {
|
||||
ChartJS.defaults.color = darkMode ? '#ccc' : '#666'
|
||||
ChartJS.defaults.borderColor = darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
||||
})
|
||||
|
||||
ChartJS.defaults.color = isDark() ? '#ccc' : '#666'
|
||||
ChartJS.defaults.borderColor = isDark() ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
||||
|
||||
function getBackgorundColor(): string {
|
||||
return isDark() ? '#2e3440' : '#ffffff'
|
||||
function getBackgroundColor(): string {
|
||||
return darkMode ? '#2e3440' : '#ffffff'
|
||||
}
|
||||
function hexToRgb(hexColor: string): number[] {
|
||||
hexColor = hexColor.replace(/^#/, '')
|
||||
@@ -150,29 +132,10 @@
|
||||
return rgbToHex(blendedRgb)
|
||||
}
|
||||
|
||||
function highlightSelectedPoints(ids: string[]) {
|
||||
if (!canSelect || ids.length === 0) {
|
||||
data.datasets[0].backgroundColor = FAIL_COLOR
|
||||
data.datasets[1].backgroundColor = SUCCESS_COLOR
|
||||
} else {
|
||||
data.datasets[0].backgroundColor = data.datasets[0].data.map((p) =>
|
||||
ids.includes(p.id) ? FAIL_COLOR : FAIL_COLOR_TRANSPARENT
|
||||
)
|
||||
data.datasets[1].backgroundColor = data.datasets[1].data.map((p) =>
|
||||
ids.includes(p.id) ? SUCCESS_COLOR : SUCCESS_COLOR_TRANSPARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getPath(x: any): string {
|
||||
return x.path
|
||||
}
|
||||
|
||||
let minTime = addSeconds(new Date(), -300)
|
||||
let maxTime = getDbClockNow()
|
||||
|
||||
$: computeMinMaxTime(jobs, minTimeSet, maxTimeSet)
|
||||
|
||||
function minJobTime(jobs: CompletedJob[]): Date {
|
||||
let min: Date = new Date(jobs[0].started_at)
|
||||
for (const job of jobs) {
|
||||
@@ -195,6 +158,7 @@
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
function computeMinMaxTime(
|
||||
jobs: CompletedJob[] | undefined,
|
||||
minTimeSet: string | undefined,
|
||||
@@ -203,15 +167,13 @@
|
||||
let minTimeSetDate = minTimeSet ? new Date(minTimeSet) : undefined
|
||||
let maxTimeSetDate = maxTimeSet ? new Date(maxTimeSet) : undefined
|
||||
if (minTimeSetDate && maxTimeSetDate) {
|
||||
minTime = minTimeSetDate
|
||||
maxTime = maxTimeSetDate
|
||||
return
|
||||
return { minTime: minTimeSetDate, maxTime: maxTimeSetDate }
|
||||
}
|
||||
|
||||
if (jobs == undefined || jobs?.length == 0) {
|
||||
minTime = minTimeSetDate ?? addSeconds(new Date(), -300)
|
||||
maxTime = maxTimeSetDate ?? getDbClockNow()
|
||||
return
|
||||
const computedMinTime = minTimeSetDate ?? addSeconds(new Date(), -300)
|
||||
const computedMaxTime = maxTimeSetDate ?? getDbClockNow()
|
||||
return { minTime: computedMinTime, maxTime: computedMaxTime }
|
||||
}
|
||||
|
||||
const maxJob = maxIsNow ? getDbClockNow() : maxJobTime(jobs)
|
||||
@@ -219,12 +181,11 @@
|
||||
|
||||
const diff = (maxJob.getTime() - minJob.getTime()) / 20000
|
||||
|
||||
minTime = minTimeSetDate ?? addSeconds(minJob, -diff)
|
||||
if (maxIsNow) {
|
||||
maxTime = maxTimeSetDate ?? maxJob
|
||||
} else {
|
||||
maxTime = maxTimeSetDate ?? addSeconds(maxJob, diff)
|
||||
}
|
||||
let computedMinTime = minTimeSetDate ?? addSeconds(minJob, -diff)
|
||||
let computedMaxTime = maxIsNow
|
||||
? (maxTimeSetDate ?? maxJob)
|
||||
: (maxTimeSetDate ?? addSeconds(maxJob, diff))
|
||||
return { minTime: computedMinTime, maxTime: computedMaxTime }
|
||||
}
|
||||
|
||||
function addSeconds(date: Date, seconds: number): Date {
|
||||
@@ -232,7 +193,56 @@
|
||||
return date
|
||||
}
|
||||
|
||||
$: scatterOptions = {
|
||||
let success = $derived(jobs?.filter((x) => x.success))
|
||||
let failed = $derived(jobs?.filter((x) => !x.success))
|
||||
let data = $derived.by(() => {
|
||||
const data = {
|
||||
datasets: [
|
||||
{
|
||||
borderColor: 'rgba(99,0,125, 0)',
|
||||
backgroundColor: FAIL_COLOR as string | string[],
|
||||
radius: 3,
|
||||
label: 'Failed',
|
||||
data:
|
||||
failed?.map((job) => ({
|
||||
x: job.started_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
})) ?? []
|
||||
},
|
||||
{
|
||||
borderColor: 'rgba(99,0,125, 0)',
|
||||
backgroundColor: SUCCESS_COLOR as string | string[],
|
||||
radius: 3,
|
||||
label: 'Successful',
|
||||
data:
|
||||
success?.map((job) => ({
|
||||
x: job.started_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
})) ?? []
|
||||
}
|
||||
]
|
||||
}
|
||||
if (!canSelect || selectedIds.length === 0) {
|
||||
data.datasets[0].backgroundColor = FAIL_COLOR
|
||||
data.datasets[1].backgroundColor = SUCCESS_COLOR
|
||||
} else {
|
||||
data.datasets[0].backgroundColor = data.datasets[0].data.map((p) =>
|
||||
selectedIds.includes(p.id) ? FAIL_COLOR : FAIL_COLOR_TRANSPARENT
|
||||
)
|
||||
data.datasets[1].backgroundColor = data.datasets[1].data.map((p) =>
|
||||
selectedIds.includes(p.id) ? SUCCESS_COLOR : SUCCESS_COLOR_TRANSPARENT
|
||||
)
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const minMaxTime = $derived.by(() => computeMinMaxTime(jobs, minTimeSet, maxTimeSet))
|
||||
|
||||
let scatterOptions = $derived({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
@@ -242,17 +252,17 @@
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (context) {
|
||||
label: function (context: any) {
|
||||
return getPath(context.raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick: (e, u) => {
|
||||
onClick: (_e: any, u: any) => {
|
||||
if (canSelect) {
|
||||
const ids = u.map((j) => data.datasets[j.datasetIndex].data[j.index].id)
|
||||
const ids = u.map((j: any) => data.datasets[j.datasetIndex].data[j.index].id)
|
||||
selectedIds = ids
|
||||
dispatch('pointClicked', ids)
|
||||
onPointClicked(ids)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -262,8 +272,8 @@
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
min: minTime,
|
||||
max: maxTime
|
||||
min: minMaxTime.minTime,
|
||||
max: minMaxTime.maxTime
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
@@ -277,11 +287,11 @@
|
||||
}
|
||||
},
|
||||
animation: false
|
||||
} as any
|
||||
|
||||
$: data && scatterOptions && highlightSelectedPoints(selectedIds)
|
||||
} as any)
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
|
||||
<!-- {JSON.stringify(minTime)}
|
||||
{JSON.stringify(maxTime)}
|
||||
|
||||
@@ -291,12 +301,8 @@
|
||||
<!-- {JSON.stringify(jobs?.map((x) => x.started_at))} -->
|
||||
<div class="relative max-h-40">
|
||||
{#if !lastFetchWentToEnd}
|
||||
<div class="absolute top-[-10px] left-[60px]"
|
||||
><Button
|
||||
size="xs"
|
||||
color="transparent"
|
||||
variant="contained"
|
||||
on:click={() => dispatch('loadExtra')}
|
||||
<div class="absolute top-[-26px] left-[160px]"
|
||||
><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
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
className = '',
|
||||
computeS3ForceViewerPolicies = undefined,
|
||||
workspace = undefined,
|
||||
actions
|
||||
actions: actions_render = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -249,8 +249,6 @@
|
||||
$effect.pre(() => {
|
||||
isValid = allTrue(inputCheck ?? {})
|
||||
})
|
||||
|
||||
const actions_render = $derived(actions)
|
||||
</script>
|
||||
|
||||
{#if showReset}
|
||||
|
||||
@@ -1791,6 +1791,7 @@
|
||||
bind:hasPreprocessor
|
||||
bind:captureTable
|
||||
bind:assets={script.assets}
|
||||
enablePreprocessorSnippet
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
disableAi?: boolean
|
||||
assets?: AssetWithAltAccessType[]
|
||||
editor_bar_right?: import('svelte').Snippet
|
||||
enablePreprocessorSnippet?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -111,7 +112,8 @@
|
||||
lastDeployedCode = undefined,
|
||||
disableAi = false,
|
||||
assets = $bindable(),
|
||||
editor_bar_right
|
||||
editor_bar_right,
|
||||
enablePreprocessorSnippet = false
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -625,6 +627,7 @@
|
||||
automaticLayout={true}
|
||||
{fixedOverflowWidgets}
|
||||
{args}
|
||||
{enablePreprocessorSnippet}
|
||||
/>
|
||||
<DiffEditor
|
||||
className="h-full"
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
export let date: string
|
||||
export let agoOnlyIfRecent: boolean = false
|
||||
export let noDate = false
|
||||
export let isRecent: boolean = true
|
||||
|
||||
let computedTimeAgo: string | undefined = undefined
|
||||
|
||||
let isRecent = true
|
||||
let interval
|
||||
|
||||
$: date && computeDate()
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
right?: string
|
||||
rightTooltip?: string
|
||||
rightDocumentationLink?: string
|
||||
title?: string
|
||||
} = {}
|
||||
export let checked: boolean = false
|
||||
export let disabled = false
|
||||
@@ -37,6 +38,7 @@
|
||||
class="{$$props.class || ''} z-auto flex flex-row items-center duration-50 {disabled
|
||||
? 'grayscale opacity-50'
|
||||
: 'cursor-pointer'}"
|
||||
title={options?.title}
|
||||
>
|
||||
{#if Boolean(options?.left)}
|
||||
<span
|
||||
@@ -103,7 +105,7 @@
|
||||
class={twMerge(
|
||||
'ml-2 font-medium duration-50 select-none',
|
||||
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
|
||||
size === 'xs' || size === '2sm' ? 'text-xs' : 'text-sm',
|
||||
size === 'xs' || size === '2sm' ? 'text-xs' : size === '2xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
style={textStyle}
|
||||
|
||||
@@ -173,11 +173,11 @@
|
||||
<Disposable
|
||||
{id}
|
||||
bind:this={disposable}
|
||||
on:open={() => {
|
||||
onOpen={() => {
|
||||
outputs?.open.set(true)
|
||||
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
on:close={() => {
|
||||
onClose={() => {
|
||||
outputs?.open.set(false)
|
||||
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { AppService, DraftService, SettingService, type Policy } from '$lib/gen'
|
||||
import { redo, undo } from '$lib/history.svelte'
|
||||
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import type { Item } from '$lib/utils'
|
||||
import {
|
||||
AlignHorizontalSpaceAround,
|
||||
BellOff,
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
} from './componentCallbacks.svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
|
||||
const { history, movingcomponents, jobsDrawerOpen } = getContext<AppEditorContext>(
|
||||
'AppEditorContext'
|
||||
) as AppEditorContext
|
||||
const { history, movingcomponents, jobsDrawerOpen, runnableJobEditorPanel } =
|
||||
getContext<AppEditorContext>('AppEditorContext') as AppEditorContext
|
||||
const { app, selectedComponent, focusedGrid, componentControl } = getContext<AppViewerContext>(
|
||||
'AppViewerContext'
|
||||
) as AppViewerContext
|
||||
@@ -33,7 +32,8 @@
|
||||
let classes = event.target?.['className']
|
||||
if (
|
||||
(typeof classes === 'string' && classes.includes('inputarea')) ||
|
||||
['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName!)
|
||||
['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName!) ||
|
||||
$runnableJobEditorPanel.focused
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ export async function handlePaste(
|
||||
} else {
|
||||
copiedGridItems = undefined
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (tempGridItems != undefined) {
|
||||
|
||||
@@ -237,6 +237,8 @@
|
||||
$effect(() => {
|
||||
items != undefined && handleItemsChange()
|
||||
})
|
||||
|
||||
const rnd = generateRandomString()
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 flex-col mt-2 w-full">
|
||||
@@ -286,7 +288,8 @@
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {}
|
||||
dropTargetStyle: {},
|
||||
type: rnd
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
})
|
||||
component.numberOfSubgrids = items.length + 1
|
||||
}
|
||||
|
||||
const rnd = generateRandomString()
|
||||
</script>
|
||||
|
||||
<PanelSection title={'Conditions'}>
|
||||
@@ -141,7 +143,8 @@
|
||||
use:dragHandleZone={{
|
||||
items: items,
|
||||
flipDurationMs: 200,
|
||||
dropTargetStyle: {}
|
||||
dropTargetStyle: {},
|
||||
type: rnd
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
|
||||
let resolvedPaths: string[] = $state([])
|
||||
let resolvedLabels: string[] = $state([])
|
||||
|
||||
const rnd = generateRandomString()
|
||||
</script>
|
||||
|
||||
<PanelSection
|
||||
@@ -134,7 +136,8 @@
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs: 200,
|
||||
dropTargetStyle: {}
|
||||
dropTargetStyle: {},
|
||||
type: rnd
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
|
||||
@@ -158,6 +158,8 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const rnd = generateRandomString()
|
||||
</script>
|
||||
|
||||
<PanelSection title={`${word}s ${tabs && tabs.length > 0 ? `(${tabs.length})` : ''}`}>
|
||||
@@ -169,7 +171,8 @@
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs: 200,
|
||||
dropTargetStyle: {}
|
||||
dropTargetStyle: {},
|
||||
type: rnd
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
|
||||
@@ -103,6 +103,8 @@
|
||||
|
||||
items = newItems
|
||||
}
|
||||
|
||||
const rnd = generateRandomString()
|
||||
</script>
|
||||
|
||||
{#if components}
|
||||
@@ -132,7 +134,8 @@
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs: 200,
|
||||
dropTargetStyle: {}
|
||||
dropTargetStyle: {},
|
||||
type: rnd
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
|
||||
@@ -65,30 +65,34 @@
|
||||
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
<ul class="flex flex-col divide-y mt-1">
|
||||
{#each assets.value ?? [] as asset}
|
||||
<li class="flex justify-between py-3 leading-4 text-sm pl-4">
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
{asset.path}
|
||||
<span class="text-2xs text-tertiary">
|
||||
{formatAssetKind({
|
||||
...asset,
|
||||
...(asset.kind === 'resource'
|
||||
? { metadata: { resource_type: resourceDataCache[asset.path] } }
|
||||
: {})
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<AssetButtons
|
||||
{asset}
|
||||
{resourceDataCache}
|
||||
{dbManagerDrawer}
|
||||
{resourceEditorDrawer}
|
||||
{s3FilePicker}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if assets.value && assets.value.length > 0}
|
||||
<ul class="flex flex-col divide-y mt-1">
|
||||
{#each assets.value ?? [] as asset}
|
||||
<li class="flex justify-between py-3 leading-4 text-sm pl-4">
|
||||
<div class="flex flex-col flex-1 truncate">
|
||||
{asset.path}
|
||||
<span class="text-2xs text-tertiary">
|
||||
{formatAssetKind({
|
||||
...asset,
|
||||
...(asset.kind === 'resource'
|
||||
? { metadata: { resource_type: resourceDataCache[asset.path] } }
|
||||
: {})
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<AssetButtons
|
||||
{asset}
|
||||
{resourceDataCache}
|
||||
{dbManagerDrawer}
|
||||
{resourceEditorDrawer}
|
||||
{s3FilePicker}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<div class="text-sm text-tertiary">No assets found</div>
|
||||
{/if}
|
||||
|
||||
<S3FilePicker bind:this={s3FilePicker} readOnlyMode />
|
||||
<DbManagerDrawer bind:this={dbManagerDrawer} />
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
export let baseClass = 'text-center'
|
||||
export let capitalize = false
|
||||
export let icon: BadgeIconProps | undefined = undefined
|
||||
export let verySmall = false
|
||||
|
||||
let hidden = false
|
||||
const colors: Record<BadgeColor, string> = {
|
||||
@@ -46,11 +47,18 @@
|
||||
|
||||
$: badgeClass = classNames(
|
||||
baseClass,
|
||||
small ? 'text-xs' : large ? 'text-sm font-medium' : 'text-xs font-semibold',
|
||||
small
|
||||
? 'text-xs'
|
||||
: verySmall
|
||||
? 'text-2xs'
|
||||
: large
|
||||
? 'text-sm font-medium'
|
||||
: 'text-xs font-semibold',
|
||||
colors[color],
|
||||
href &&
|
||||
(color.startsWith(ColorModifier) ? hovers[color.replace(ColorModifier, '')] : hovers[color]),
|
||||
rounded ? 'rounded-full px-2 py-1' : 'rounded px-2.5 py-0.5',
|
||||
verySmall ? 'px-0.5 py-0.5' : '',
|
||||
'flex flex-row gap-1 items-center',
|
||||
$$props.class
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import DateTimeInput from '$lib/components/DateTimeInput.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let date: string | undefined
|
||||
export let label: string
|
||||
@@ -25,7 +26,10 @@
|
||||
<svelte:fragment slot="trigger">
|
||||
<button
|
||||
title="Open calendar picker"
|
||||
class="absolute bottom-1 right-2 top-1 py-1 min-w-min !px-2 items-center text-primary bg-surface border rounded center-center hover:bg-surface-hover transition-all cursor-pointer"
|
||||
class={twMerge(
|
||||
'absolute bottom-1 right-2 top-1 py-1 min-w-min !px-2 items-center text-primary bg-surface border rounded center-center hover:bg-surface-hover transition-all cursor-pointer',
|
||||
$$props.class
|
||||
)}
|
||||
aria-label="Open calendar picker"
|
||||
on:click={() => {
|
||||
input?.focus()
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
<script lang="ts" context="module">
|
||||
export let openedDrawers: string[] = []
|
||||
<script lang="ts" module>
|
||||
export let openedDrawers: { val: string[] } = $state({ val: [] })
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
||||
|
||||
import { zIndexes } from '$lib/zIndexes'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let open = false
|
||||
export let id = (Math.random() + 1).toString(36).substring(10)
|
||||
export let preventEscape = false
|
||||
|
||||
if (open) {
|
||||
openedDrawers.push(id)
|
||||
interface Props {
|
||||
open?: boolean
|
||||
id?: any
|
||||
preventEscape?: boolean
|
||||
initialOffset?: number
|
||||
children?: import('svelte').Snippet<[any]>
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export let initialOffset = 0
|
||||
let {
|
||||
open = $bindable(false),
|
||||
id = (Math.random() + 1).toString(36).substring(10),
|
||||
preventEscape = false,
|
||||
initialOffset = 0,
|
||||
children,
|
||||
onOpen,
|
||||
onClose
|
||||
}: Props = $props()
|
||||
|
||||
let offset = initialOffset
|
||||
let offset = $state(initialOffset)
|
||||
let zIndex = $derived(zIndexes.disposables + offset)
|
||||
|
||||
export function toggleDrawer() {
|
||||
if (!open) {
|
||||
@@ -30,32 +38,31 @@
|
||||
}
|
||||
|
||||
export function openDrawer() {
|
||||
open = true
|
||||
offset = openedDrawers.length - 1
|
||||
|
||||
if (openedDrawers.includes(id)) {
|
||||
return
|
||||
if (!open) {
|
||||
open = true
|
||||
if (openedDrawers.val.includes(id)) {
|
||||
return
|
||||
}
|
||||
offset = openedDrawers.val.length - 1
|
||||
openedDrawers.val.push(id)
|
||||
}
|
||||
|
||||
openedDrawers.push(id)
|
||||
}
|
||||
|
||||
export function closeDrawer() {
|
||||
open = false
|
||||
offset = initialOffset
|
||||
// remove the last opened drawer
|
||||
openedDrawers = openedDrawers.filter((drawer) => drawer !== id)
|
||||
if (open) {
|
||||
open = false
|
||||
offset = initialOffset
|
||||
// remove the last opened drawer
|
||||
openedDrawers.val = openedDrawers.val.filter((drawer) => drawer !== id)
|
||||
}
|
||||
}
|
||||
|
||||
export function isOpen() {
|
||||
return open
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
|
||||
|
||||
function handleClickAway(e) {
|
||||
const last = openedDrawers[openedDrawers.length - 1]
|
||||
const last = openedDrawers.val[openedDrawers.val.length - 1]
|
||||
|
||||
if (last === id) {
|
||||
e.stopPropagation()
|
||||
@@ -68,10 +75,11 @@
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
if (
|
||||
(id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) &&
|
||||
(id == openedDrawers.val[openedDrawers.val.length - 1] ||
|
||||
openedDrawers.val.length == 0) &&
|
||||
!preventEscape
|
||||
) {
|
||||
openedDrawers.pop()
|
||||
openedDrawers.val.pop()
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.stopImmediatePropagation()
|
||||
@@ -82,11 +90,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: zIndex = zIndexes.disposables + offset
|
||||
if (open) {
|
||||
openedDrawers.val.push(id)
|
||||
}
|
||||
|
||||
$: dispatchIfMounted(open ? 'open' : 'close')
|
||||
let wasEverOpen = false
|
||||
let lastOpen = open
|
||||
$effect.pre(() => {
|
||||
if (open === untrack(() => lastOpen)) {
|
||||
return
|
||||
}
|
||||
lastOpen = open
|
||||
if (open) {
|
||||
// console.log('open', id, wasEverOpen)
|
||||
wasEverOpen = true
|
||||
onOpen?.()
|
||||
} else if (untrack(() => wasEverOpen)) {
|
||||
// console.log('close', id)
|
||||
onClose?.()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
|
||||
<slot {handleClickAway} {zIndex} {closeDrawer} {open} />
|
||||
{@render children?.({
|
||||
handleClickAway,
|
||||
zIndex,
|
||||
closeDrawer,
|
||||
open,
|
||||
isTop: openedDrawers.val[openedDrawers.val.length - 1] == id
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from 'svelte'
|
||||
import { onMount, createEventDispatcher, untrack } from 'svelte'
|
||||
import { BROWSER } from 'esm-env'
|
||||
import Disposable from './Disposable.svelte'
|
||||
import ConditionalPortal from './ConditionalPortal.svelte'
|
||||
@@ -32,7 +32,7 @@
|
||||
disableChatOffset = false,
|
||||
class: clazz = '',
|
||||
positionClass = undefined,
|
||||
children
|
||||
children: children_render
|
||||
}: Props = $props()
|
||||
|
||||
if (open === undefined) {
|
||||
@@ -52,11 +52,12 @@
|
||||
}
|
||||
|
||||
export function closeDrawer() {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
dispatch('afterClose')
|
||||
}, durationMs)
|
||||
}
|
||||
disposable?.closeDrawer()
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch('afterClose')
|
||||
}, durationMs)
|
||||
}
|
||||
|
||||
export function isOpen() {
|
||||
@@ -83,7 +84,10 @@
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
open ? openDrawer() : closeDrawer()
|
||||
open
|
||||
untrack(() => {
|
||||
open ? openDrawer() : closeDrawer()
|
||||
})
|
||||
})
|
||||
|
||||
let timeout = $state(true)
|
||||
@@ -94,7 +98,6 @@
|
||||
mounted = true
|
||||
})
|
||||
|
||||
const children_render = $derived(children)
|
||||
const aiChatOpen = $derived(chatState.size > 0)
|
||||
</script>
|
||||
|
||||
@@ -103,11 +106,11 @@
|
||||
initialOffset={offset}
|
||||
bind:open
|
||||
bind:this={disposable}
|
||||
on:open
|
||||
on:close
|
||||
onOpen={() => dispatch('open')}
|
||||
onClose={() => dispatch('close')}
|
||||
{preventEscape}
|
||||
>
|
||||
{#snippet children({ handleClickAway, zIndex })}
|
||||
{#snippet children({ handleClickAway, zIndex, isTop })}
|
||||
<aside
|
||||
class="drawer windmill-app windmill-drawer {clazz ?? ''} {positionClass ?? ''} {aiChatOpen
|
||||
? 'respect-global-chat'
|
||||
@@ -122,7 +125,7 @@
|
||||
<div class="overlay {positionClass ?? ''}" onclick={handleClickAway}></div>
|
||||
<div class="panel {placement} {positionClass}" class:size>
|
||||
{#if open || !timeout || alwaysOpen}
|
||||
{@render children_render?.({ open })}
|
||||
{@render children_render?.({ open, isTop })}
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
import type { TabsContext } from '$lib/components/apps/editor/settingsPanel/inputEditor/tabs.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
export let value: string
|
||||
export let alwaysMounted: boolean = false
|
||||
export let style = ''
|
||||
interface Props {
|
||||
value: string
|
||||
alwaysMounted?: boolean
|
||||
style?: string
|
||||
values?: string[] | undefined
|
||||
class?: string
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
export let values: string[] | undefined = undefined
|
||||
let {
|
||||
value,
|
||||
alwaysMounted = false,
|
||||
style = '',
|
||||
values = undefined,
|
||||
class: clazz = '',
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
const { selected } = getContext<TabsContext>('Tabs')
|
||||
</script>
|
||||
|
||||
@@ -18,6 +28,6 @@
|
||||
class={`${clazz} ${value === $selected || values?.includes($selected) ? 'visible' : 'hidden'}`}
|
||||
{style}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
type TogglableItem = {
|
||||
label: string
|
||||
value: string
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -29,11 +30,11 @@
|
||||
togglableItems
|
||||
}: Props = $props()
|
||||
|
||||
function select(v: string) {
|
||||
selected = v
|
||||
}
|
||||
|
||||
let items = togglableItems.map((i) => ({ displayName: i.label, action: () => select(i.value) }))
|
||||
let items = togglableItems.map((i) => ({
|
||||
displayName: i.label,
|
||||
action: () => (selected = i.value),
|
||||
tooltip: i.tooltip
|
||||
}))
|
||||
|
||||
function isAnOptionSelected(selected: string | undefined) {
|
||||
return togglableItems.some((i) => i.value === selected)
|
||||
@@ -48,6 +49,7 @@
|
||||
>
|
||||
<div {id} class="flex">
|
||||
{#if isAnOptionSelected(selected)}
|
||||
{@const tooltip = togglableItems.find((i) => i.value === selected)?.tooltip}
|
||||
<ToggleButton
|
||||
{disabled}
|
||||
value={selected ?? ''}
|
||||
@@ -56,6 +58,8 @@
|
||||
{light}
|
||||
{id}
|
||||
label={togglableItems.find((i) => i.value === selected)?.label}
|
||||
{tooltip}
|
||||
showTooltipIcon={!!tooltip}
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex items-center">
|
||||
|
||||
@@ -11,16 +11,13 @@ import HistoryManager from './HistoryManager.svelte'
|
||||
import {
|
||||
extractCodeFromMarkdown,
|
||||
getLatestAssistantMessage,
|
||||
processToolCall,
|
||||
type DisplayMessage,
|
||||
type Tool,
|
||||
type ToolCallbacks,
|
||||
type ToolDisplayMessage
|
||||
} from './shared'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionUserMessageParam
|
||||
} from 'openai/resources/chat/completions.mjs'
|
||||
@@ -34,7 +31,7 @@ import { loadApiTools } from './api/apiTools'
|
||||
import { prepareScriptUserMessage } from './script/core'
|
||||
import { prepareNavigatorUserMessage } from './navigator/core'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getCompletion, getModelContextWindow } from '../lib'
|
||||
import { getCompletion, getModelContextWindow, parseOpenAICompletion } from '../lib'
|
||||
import { dfs } from '$lib/components/flows/previousResults'
|
||||
import { getStringError } from './utils'
|
||||
import type { FlowModuleState, FlowState } from '$lib/components/flows/flowState'
|
||||
@@ -47,6 +44,7 @@ import type { ContextElement } from './context'
|
||||
import type { Selection } from 'monaco-editor'
|
||||
import type AIChatInput from './AIChatInput.svelte'
|
||||
import { prepareApiSystemMessage, prepareApiUserMessage } from './api/core'
|
||||
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
|
||||
|
||||
// If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message
|
||||
const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05
|
||||
@@ -380,10 +378,8 @@ class AIChatManager {
|
||||
}
|
||||
systemMessage?: ChatCompletionSystemMessageParam
|
||||
}) => {
|
||||
let addedMessages: ChatCompletionMessageParam[] = []
|
||||
try {
|
||||
let completion: any = null
|
||||
|
||||
let addedMessages: ChatCompletionMessageParam[] = []
|
||||
while (true) {
|
||||
const systemMessage = systemMessageOverride ?? this.systemMessage
|
||||
const helpers = this.helpers
|
||||
@@ -413,99 +409,28 @@ class AIChatManager {
|
||||
}
|
||||
this.pendingPrompt = ''
|
||||
}
|
||||
completion = await getCompletion(
|
||||
|
||||
const model = getCurrentModel()
|
||||
const completionFn = model.provider === 'anthropic' ? getAnthropicCompletion : getCompletion
|
||||
const parseFn =
|
||||
model.provider === 'anthropic' ? parseAnthropicCompletion : parseOpenAICompletion
|
||||
|
||||
const completion = await completionFn(
|
||||
[systemMessage, ...messages, ...(pendingUserMessage ? [pendingUserMessage] : [])],
|
||||
abortController,
|
||||
tools.map((t) => t.def)
|
||||
)
|
||||
|
||||
if (completion) {
|
||||
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
|
||||
|
||||
let answer = ''
|
||||
for await (const chunk of completion) {
|
||||
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
|
||||
continue
|
||||
}
|
||||
const c = chunk as ChatCompletionChunk
|
||||
const delta = c.choices[0].delta.content
|
||||
if (delta) {
|
||||
answer += delta
|
||||
callbacks.onNewToken(delta)
|
||||
}
|
||||
const toolCalls = c.choices[0].delta.tool_calls || []
|
||||
if (toolCalls.length > 0 && answer) {
|
||||
// if tool calls are present but we have some textual content already, we need to display it to the user first
|
||||
callbacks.onMessageEnd()
|
||||
answer = ''
|
||||
}
|
||||
for (const toolCall of toolCalls) {
|
||||
const { index } = toolCall
|
||||
let finalToolCall = finalToolCalls[index]
|
||||
if (!finalToolCall) {
|
||||
finalToolCalls[index] = toolCall
|
||||
} else {
|
||||
if (toolCall.function?.arguments) {
|
||||
if (!finalToolCall.function) {
|
||||
finalToolCall.function = toolCall.function
|
||||
} else {
|
||||
finalToolCall.function.arguments =
|
||||
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
finalToolCall = finalToolCalls[index]
|
||||
if (finalToolCall?.function) {
|
||||
const {
|
||||
function: { name: funcName },
|
||||
id: toolCallId
|
||||
} = finalToolCall
|
||||
if (funcName && toolCallId) {
|
||||
const tool = tools.find((t) => t.def.function.name === funcName)
|
||||
if (tool && tool.preAction) {
|
||||
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
const toAdd = { role: 'assistant' as const, content: answer }
|
||||
addedMessages.push(toAdd)
|
||||
messages.push(toAdd)
|
||||
}
|
||||
|
||||
callbacks.onMessageEnd()
|
||||
|
||||
const toolCalls = Object.values(finalToolCalls).filter(
|
||||
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
|
||||
) as ChatCompletionMessageToolCall[]
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const toAdd = {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: toolCalls.map((t) => ({
|
||||
...t,
|
||||
function: {
|
||||
...t.function,
|
||||
arguments: t.function.arguments || '{}'
|
||||
}
|
||||
}))
|
||||
}
|
||||
messages.push(toAdd)
|
||||
addedMessages.push(toAdd)
|
||||
for (const toolCall of toolCalls) {
|
||||
const messageToAdd = await processToolCall({
|
||||
tools,
|
||||
toolCall,
|
||||
helpers,
|
||||
toolCallbacks: callbacks
|
||||
})
|
||||
messages.push(messageToAdd)
|
||||
addedMessages.push(messageToAdd)
|
||||
}
|
||||
} else {
|
||||
const continueCompletion = await parseFn(
|
||||
completion as any,
|
||||
callbacks,
|
||||
messages,
|
||||
addedMessages,
|
||||
tools,
|
||||
helpers
|
||||
)
|
||||
if (!continueCompletion) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import { OpenAI } from 'openai'
|
||||
import type {
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageFunctionToolCall
|
||||
} from 'openai/resources/index.mjs'
|
||||
import type {
|
||||
MessageParam,
|
||||
TextBlockParam,
|
||||
ToolUnion,
|
||||
ToolUseBlockParam,
|
||||
Tool as AnthropicTool,
|
||||
Message
|
||||
} from '@anthropic-ai/sdk/resources'
|
||||
import type { MessageStream } from '@anthropic-ai/sdk/lib/MessageStream'
|
||||
import { getProviderAndCompletionConfig, workspaceAIClients } from '../lib'
|
||||
import { processToolCall, type Tool, type ToolCallbacks } from './shared'
|
||||
|
||||
export async function getAnthropicCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionFunctionTool[]
|
||||
): Promise<MessageStream> {
|
||||
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true })
|
||||
const { system, messages: anthropicMessages } = convertOpenAIToAnthropicMessages(messages)
|
||||
const anthropicTools = convertOpenAIToolsToAnthropic(tools)
|
||||
|
||||
const anthropicClient = workspaceAIClients.getAnthropicClient()
|
||||
|
||||
const anthropicParams = {
|
||||
model: config.model,
|
||||
max_tokens: config.max_tokens as number,
|
||||
messages: anthropicMessages,
|
||||
...(system && { system }),
|
||||
...(anthropicTools && { tools: anthropicTools }),
|
||||
...(typeof config.temperature === 'number' && { temperature: config.temperature })
|
||||
}
|
||||
|
||||
const stream = anthropicClient.messages.stream(anthropicParams, {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': provider,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'X-Anthropic-SDK': 'true'
|
||||
}
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
export async function parseAnthropicCompletion(
|
||||
completion: MessageStream,
|
||||
callbacks: ToolCallbacks & {
|
||||
onNewToken: (token: string) => void
|
||||
onMessageEnd: () => void
|
||||
},
|
||||
messages: ChatCompletionMessageParam[],
|
||||
addedMessages: ChatCompletionMessageParam[],
|
||||
tools: Tool<any>[],
|
||||
helpers: any
|
||||
): Promise<boolean> {
|
||||
let toolCallsToProcess: ChatCompletionMessageFunctionToolCall[] = []
|
||||
let error = null
|
||||
|
||||
// Handle text streaming
|
||||
completion.on('text', (textDelta: string, _textSnapshot: string) => {
|
||||
callbacks.onNewToken(textDelta)
|
||||
})
|
||||
|
||||
completion.on('message', (message: Message) => {
|
||||
for (const block of message.content) {
|
||||
if (block.type === 'text') {
|
||||
const text = block.text
|
||||
const assistantMessage = { role: 'assistant' as const, content: text }
|
||||
messages.push(assistantMessage)
|
||||
addedMessages.push(assistantMessage)
|
||||
callbacks.onMessageEnd()
|
||||
} else if (block.type === 'tool_use') {
|
||||
// Convert Anthropic tool calls to OpenAI format for compatibility
|
||||
toolCallsToProcess.push({
|
||||
id: block.id,
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input)
|
||||
}
|
||||
})
|
||||
// Preprocess tool if it has a preAction
|
||||
const tool = tools.find((t) => t.def.function.name === block.name)
|
||||
if (tool && tool.preAction) {
|
||||
tool.preAction({ toolCallbacks: callbacks, toolId: block.id })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle errors
|
||||
completion.on('error', (e: any) => {
|
||||
console.error('Anthropic stream error:', e)
|
||||
error = e
|
||||
})
|
||||
|
||||
// Wait for completion
|
||||
await completion.done()
|
||||
|
||||
callbacks.onMessageEnd()
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// Process tool calls if any
|
||||
if (toolCallsToProcess.length > 0) {
|
||||
const assistantWithTools = {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: toolCallsToProcess
|
||||
}
|
||||
messages.push(assistantWithTools)
|
||||
addedMessages.push(assistantWithTools)
|
||||
|
||||
// Process each tool call
|
||||
for (const toolCall of toolCallsToProcess) {
|
||||
const messageToAdd = await processToolCall({
|
||||
tools,
|
||||
toolCall,
|
||||
helpers,
|
||||
toolCallbacks: callbacks
|
||||
})
|
||||
messages.push(messageToAdd)
|
||||
addedMessages.push(messageToAdd)
|
||||
}
|
||||
return true // Continue the conversation loop
|
||||
}
|
||||
|
||||
return false // End the conversation
|
||||
}
|
||||
|
||||
export function convertOpenAIToAnthropicMessages(messages: ChatCompletionMessageParam[]): {
|
||||
system: TextBlockParam[] | undefined
|
||||
messages: MessageParam[]
|
||||
} {
|
||||
let system: TextBlockParam[] | undefined
|
||||
const anthropicMessages: MessageParam[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === 'system') {
|
||||
const systemText =
|
||||
typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
|
||||
// Convert system to array format with cache_control for caching
|
||||
system = [
|
||||
{
|
||||
type: 'text',
|
||||
text: systemText,
|
||||
cache_control: { type: 'ephemeral' }
|
||||
}
|
||||
]
|
||||
continue
|
||||
}
|
||||
|
||||
if (message.role === 'user') {
|
||||
anthropicMessages.push({
|
||||
role: 'user',
|
||||
content:
|
||||
typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
|
||||
})
|
||||
} else if (message.role === 'assistant') {
|
||||
const content: (TextBlockParam | ToolUseBlockParam)[] = []
|
||||
|
||||
if (message.content) {
|
||||
content.push({
|
||||
type: 'text',
|
||||
text:
|
||||
typeof message.content === 'string' ? message.content : JSON.stringify(message.content)
|
||||
})
|
||||
}
|
||||
|
||||
if (message.tool_calls) {
|
||||
for (const toolCall of message.tool_calls) {
|
||||
if (toolCall.type !== 'function') continue
|
||||
let input = {}
|
||||
try {
|
||||
input = JSON.parse(toolCall.function.arguments || '{}')
|
||||
} catch (e) {
|
||||
console.error('Failed to parse tool call arguments', e)
|
||||
}
|
||||
content.push({
|
||||
type: 'tool_use',
|
||||
id: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
input
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (content.length > 0) {
|
||||
anthropicMessages.push({
|
||||
role: 'assistant',
|
||||
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
|
||||
})
|
||||
}
|
||||
} else if (message.role === 'tool') {
|
||||
// Tool results must be in user messages in Anthropic format
|
||||
anthropicMessages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: message.tool_call_id,
|
||||
content:
|
||||
typeof message.content === 'string'
|
||||
? message.content
|
||||
: JSON.stringify(message.content)
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add cache_control to the last message content blocks
|
||||
if (anthropicMessages.length > 0) {
|
||||
const lastMessage = anthropicMessages[anthropicMessages.length - 1]
|
||||
if (Array.isArray(lastMessage.content)) {
|
||||
// Add cache_control to the last content block
|
||||
if (lastMessage.content.length > 0) {
|
||||
const lastBlock = lastMessage.content[lastMessage.content.length - 1]
|
||||
if (lastBlock.type === 'text') {
|
||||
lastBlock.cache_control = { type: 'ephemeral' }
|
||||
}
|
||||
}
|
||||
} else if (typeof lastMessage.content === 'string') {
|
||||
// Convert string content to array format with cache_control
|
||||
lastMessage.content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: lastMessage.content,
|
||||
cache_control: { type: 'ephemeral' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return { system, messages: anthropicMessages }
|
||||
}
|
||||
|
||||
export function convertOpenAIToolsToAnthropic(
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionFunctionTool[]
|
||||
): ToolUnion[] | undefined {
|
||||
if (!tools || tools.length === 0) return undefined
|
||||
|
||||
const anthropicTools: ToolUnion[] = tools.map((tool) => ({
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
input_schema: (tool.function.parameters || {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}) as AnthropicTool.InputSchema
|
||||
}))
|
||||
|
||||
// Add cache_control to the last tool to cache all tool definitions
|
||||
if (anthropicTools.length > 0) {
|
||||
anthropicTools[anthropicTools.length - 1].cache_control = { type: 'ephemeral' }
|
||||
}
|
||||
|
||||
return anthropicTools
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ChatCompletionTool } from 'openai/resources/index.mjs'
|
||||
import type { ChatCompletionFunctionTool } from 'openai/resources/index.mjs'
|
||||
import type { Tool } from '../shared'
|
||||
import { get } from 'svelte/store'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import type { EndpointTool } from '$lib/gen/types.gen'
|
||||
import { McpService } from '$lib/gen/services.gen'
|
||||
|
||||
function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
|
||||
function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionFunctionTool {
|
||||
// Build the parameters schema for OpenAI function calling
|
||||
const parameters: Record<string, any> = {
|
||||
type: 'object',
|
||||
@@ -18,10 +18,13 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
|
||||
for (const [key, schema] of Object.entries(endpointTool.path_params_schema.properties)) {
|
||||
// Skip workspace parameter as it's auto-filled
|
||||
if (key === 'workspace') continue
|
||||
|
||||
|
||||
parameters.properties[key] = schema
|
||||
|
||||
if (Array.isArray(endpointTool.path_params_schema.required) && endpointTool.path_params_schema.required.includes(key)) {
|
||||
|
||||
if (
|
||||
Array.isArray(endpointTool.path_params_schema.required) &&
|
||||
endpointTool.path_params_schema.required.includes(key)
|
||||
) {
|
||||
parameters.required.push(key)
|
||||
}
|
||||
}
|
||||
@@ -31,8 +34,11 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
|
||||
if (endpointTool.query_params_schema?.properties) {
|
||||
for (const [key, schema] of Object.entries(endpointTool.query_params_schema.properties)) {
|
||||
parameters.properties[key] = schema
|
||||
|
||||
if (Array.isArray(endpointTool.query_params_schema.required) && endpointTool.query_params_schema.required.includes(key)) {
|
||||
|
||||
if (
|
||||
Array.isArray(endpointTool.query_params_schema.required) &&
|
||||
endpointTool.query_params_schema.required.includes(key)
|
||||
) {
|
||||
parameters.required.push(key)
|
||||
}
|
||||
}
|
||||
@@ -47,8 +53,11 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
|
||||
properties: endpointTool.body_schema.properties,
|
||||
required: endpointTool.body_schema.required || []
|
||||
}
|
||||
|
||||
if (Array.isArray(endpointTool.body_schema.required) && endpointTool.body_schema.required.length > 0) {
|
||||
|
||||
if (
|
||||
Array.isArray(endpointTool.body_schema.required) &&
|
||||
endpointTool.body_schema.required.length > 0
|
||||
) {
|
||||
parameters.required.push('body')
|
||||
}
|
||||
}
|
||||
@@ -63,16 +72,17 @@ function buildApiCallTool(endpointTool: EndpointTool): ChatCompletionTool {
|
||||
}
|
||||
}
|
||||
|
||||
function buildToolsFromEndpoints(
|
||||
endpointTools: EndpointTool[]
|
||||
): { tools: ChatCompletionTool[]; endpointMap: Record<string, { method: string; path: string }> } {
|
||||
const tools: ChatCompletionTool[] = []
|
||||
function buildToolsFromEndpoints(endpointTools: EndpointTool[]): {
|
||||
tools: ChatCompletionFunctionTool[]
|
||||
endpointMap: Record<string, { method: string; path: string }>
|
||||
} {
|
||||
const tools: ChatCompletionFunctionTool[] = []
|
||||
const endpointMap: Record<string, { method: string; path: string }> = {}
|
||||
|
||||
for (const endpointTool of endpointTools) {
|
||||
const tool = buildApiCallTool(endpointTool)
|
||||
tools.push(tool)
|
||||
|
||||
|
||||
// Store the endpoint info in the map
|
||||
endpointMap[endpointTool.name] = {
|
||||
method: endpointTool.method,
|
||||
@@ -84,17 +94,17 @@ function buildToolsFromEndpoints(
|
||||
}
|
||||
|
||||
export function createApiTools(
|
||||
chatTools: ChatCompletionTool[],
|
||||
chatTools: ChatCompletionFunctionTool[],
|
||||
endpointMap: Record<string, { method: string; path: string }> = {}
|
||||
): Tool<{}>[] {
|
||||
return chatTools.map((chatTool) => {
|
||||
const toolName = chatTool.function.name
|
||||
const endpoint = endpointMap[toolName]
|
||||
const method = endpoint?.method?.toUpperCase() || 'GET'
|
||||
|
||||
|
||||
// Determine if tool needs confirmation based on method
|
||||
const needsConfirmation = ['DELETE', 'POST', 'PUT', 'PATCH'].includes(method)
|
||||
|
||||
|
||||
return {
|
||||
def: chatTool,
|
||||
requiresConfirmation: needsConfirmation,
|
||||
@@ -103,7 +113,7 @@ export function createApiTools(
|
||||
fn: async ({ args, toolId, toolCallbacks }) => {
|
||||
const toolName = chatTool.function.name
|
||||
const endpoint = endpointMap[toolName]
|
||||
|
||||
|
||||
if (!endpoint) {
|
||||
throw new Error(`No endpoint mapping found for tool ${toolName}`)
|
||||
}
|
||||
@@ -111,7 +121,7 @@ export function createApiTools(
|
||||
try {
|
||||
const workspace = get(workspaceStore) as string
|
||||
let path = endpoint.path.replace('{workspace}', workspace)
|
||||
|
||||
|
||||
// Build URL with path parameters
|
||||
let url = `/api${path}`
|
||||
const queryParams: Record<string, string> = {}
|
||||
@@ -143,7 +153,7 @@ export function createApiTools(
|
||||
}
|
||||
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Calling ${toolName}...`,
|
||||
content: `Calling ${toolName}...`
|
||||
})
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
@@ -173,7 +183,7 @@ export function createApiTools(
|
||||
})
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Call to ${toolName} completed`,
|
||||
result: jsonResult,
|
||||
result: jsonResult
|
||||
})
|
||||
return jsonResult
|
||||
} else {
|
||||
@@ -186,7 +196,7 @@ export function createApiTools(
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Call to ${toolName} failed`,
|
||||
result: jsonResult,
|
||||
error: `HTTP ${response.status}: ${text}`,
|
||||
error: `HTTP ${response.status}: ${text}`
|
||||
})
|
||||
return jsonResult
|
||||
}
|
||||
@@ -194,7 +204,7 @@ export function createApiTools(
|
||||
const errorMessage = `Error calling API: ${error instanceof Error ? error.message : String(error)}`
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Call to ${toolName} failed`,
|
||||
error: errorMessage,
|
||||
error: errorMessage
|
||||
})
|
||||
console.error(`Error calling API:`, error)
|
||||
return errorMessage
|
||||
@@ -210,10 +220,10 @@ export async function loadApiTools(): Promise<Tool<{}>[]> {
|
||||
const endpointTools = await McpService.listMcpTools({
|
||||
workspace: get(workspaceStore) as string
|
||||
})
|
||||
|
||||
|
||||
// Build tools from the endpoint definitions
|
||||
const { tools: apiTools, endpointMap } = buildToolsFromEndpoints(endpointTools)
|
||||
|
||||
|
||||
// Create executable tools
|
||||
const executableApiTools = createApiTools(apiTools, endpointMap)
|
||||
return executableApiTools
|
||||
@@ -221,4 +231,4 @@ export async function loadApiTools(): Promise<Tool<{}>[]> {
|
||||
console.error('Failed to load API tools:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,7 +638,10 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
def: setForLoopOptionsToolDef,
|
||||
def: {
|
||||
...setForLoopOptionsToolDef,
|
||||
function: { ...setForLoopOptionsToolDef.function, strict: false }
|
||||
},
|
||||
fn: async ({ args, helpers, toolId, toolCallbacks }) => {
|
||||
const parsedArgs = setForLoopOptionsSchema.parse(args)
|
||||
await helpers.setForLoopOptions(parsedArgs.id, {
|
||||
@@ -656,7 +659,10 @@ export const flowTools: Tool<FlowAIChatHelpers>[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
def: setModuleControlOptionsToolDef,
|
||||
def: {
|
||||
...setModuleControlOptionsToolDef,
|
||||
function: { ...setModuleControlOptionsToolDef.function, strict: false }
|
||||
},
|
||||
fn: async ({ args, helpers, toolId, toolCallbacks }) => {
|
||||
const parsedArgs = setModuleControlOptionsSchema.parse(args)
|
||||
await helpers.setModuleControlOptions(parsedArgs.id, {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { get } from 'svelte/store'
|
||||
import { compile, phpCompile, pythonCompile } from '../../utils'
|
||||
import type {
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionTool,
|
||||
ChatCompletionFunctionTool,
|
||||
ChatCompletionUserMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
import { type DBSchema, dbSchemas, getCurrentModel } from '$lib/stores'
|
||||
@@ -498,7 +498,7 @@ export function prepareScriptUserMessage(
|
||||
}
|
||||
}
|
||||
|
||||
const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionTool = {
|
||||
const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionFunctionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search_resource_types',
|
||||
@@ -519,7 +519,7 @@ const RESOURCE_TYPE_FUNCTION_DEF: ChatCompletionTool = {
|
||||
}
|
||||
}
|
||||
|
||||
const DB_SCHEMA_FUNCTION_DEF: ChatCompletionTool = {
|
||||
const DB_SCHEMA_FUNCTION_DEF: ChatCompletionFunctionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_db_schema',
|
||||
@@ -693,7 +693,7 @@ export async function searchExternalIntegrationResources(args: { query: string }
|
||||
}
|
||||
}
|
||||
|
||||
const SEARCH_NPM_PACKAGES_TOOL: ChatCompletionTool = {
|
||||
const SEARCH_NPM_PACKAGES_TOOL: ChatCompletionFunctionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search_npm_packages',
|
||||
@@ -778,7 +778,7 @@ export async function fetchNpmPackageTypes(
|
||||
}
|
||||
}
|
||||
|
||||
const TEST_RUN_SCRIPT_TOOL: ChatCompletionTool = {
|
||||
const TEST_RUN_SCRIPT_TOOL: ChatCompletionFunctionTool = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'test_run_script',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionMessageToolCall,
|
||||
ChatCompletionTool
|
||||
ChatCompletionFunctionTool,
|
||||
ChatCompletionMessageFunctionToolCall,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/chat/completions.mjs'
|
||||
import { get } from 'svelte/store'
|
||||
import type { CodePieceElement, ContextElement, FlowModuleCodePieceElement } from './context'
|
||||
@@ -257,7 +257,7 @@ export async function processToolCall<T>({
|
||||
toolCallbacks
|
||||
}: {
|
||||
tools: Tool<T>[]
|
||||
toolCall: ChatCompletionMessageToolCall
|
||||
toolCall: ChatCompletionMessageFunctionToolCall
|
||||
helpers: T
|
||||
toolCallbacks: ToolCallbacks
|
||||
}): Promise<ChatCompletionMessageParam> {
|
||||
@@ -345,7 +345,7 @@ export async function processToolCall<T>({
|
||||
}
|
||||
|
||||
export interface Tool<T> {
|
||||
def: ChatCompletionTool
|
||||
def: ChatCompletionFunctionTool
|
||||
fn: (p: {
|
||||
args: any
|
||||
workspace: string
|
||||
@@ -369,7 +369,7 @@ export function createToolDef(
|
||||
zodSchema: z.ZodSchema,
|
||||
name: string,
|
||||
description: string
|
||||
): ChatCompletionTool {
|
||||
): ChatCompletionFunctionTool {
|
||||
const schema = zodToJsonSchema(zodSchema, {
|
||||
name,
|
||||
target: 'openAi'
|
||||
@@ -438,7 +438,7 @@ export const createSearchHubScriptsTool = (withContent: boolean = false) => ({
|
||||
})
|
||||
|
||||
export async function buildSchemaForTool(
|
||||
toolDef: ChatCompletionTool,
|
||||
toolDef: ChatCompletionFunctionTool,
|
||||
schemaBuilder: () => Promise<FunctionParameters>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
@@ -586,7 +586,10 @@ function getErrorMessage(result: unknown): string {
|
||||
}
|
||||
|
||||
// Build test run args based on the tool definition, if it contains a fallback schema
|
||||
export async function buildTestRunArgs(args: any, toolDef: ChatCompletionTool): Promise<any> {
|
||||
export async function buildTestRunArgs(
|
||||
args: any,
|
||||
toolDef: ChatCompletionFunctionTool
|
||||
): Promise<any> {
|
||||
let parsedArgs = args
|
||||
// if the schema is the fallback schema, parse the args as a JSON string
|
||||
if (
|
||||
|
||||
@@ -7,18 +7,23 @@ import {
|
||||
type SQLSchema
|
||||
} from '$lib/stores'
|
||||
import { buildClientSchema, printSchema } from 'graphql'
|
||||
import { OpenAI } from 'openai'
|
||||
import OpenAI from 'openai'
|
||||
import type {
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionCreateParams,
|
||||
ChatCompletionCreateParamsNonStreaming,
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
ChatCompletionMessageFunctionToolCall,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
import { formatResourceTypes } from './utils'
|
||||
import { z } from 'zod'
|
||||
import { processToolCall, type Tool, type ToolCallbacks } from './chat/shared'
|
||||
import type { Stream } from 'openai/core/streaming.mjs'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
|
||||
|
||||
@@ -219,9 +224,11 @@ export const PROVIDER_COMPLETION_CONFIG_MAP: Record<AIProvider, ChatCompletionCr
|
||||
|
||||
class WorkspacedAIClients {
|
||||
private openaiClient: OpenAI | undefined
|
||||
private anthropicClient: Anthropic | undefined
|
||||
|
||||
init(workspace: string) {
|
||||
this.initOpenai(workspace)
|
||||
this.initAnthropic(workspace)
|
||||
}
|
||||
|
||||
private getBaseURL(workspace: string) {
|
||||
@@ -240,12 +247,28 @@ class WorkspacedAIClients {
|
||||
})
|
||||
}
|
||||
|
||||
private initAnthropic(workspace: string) {
|
||||
const baseURL = this.getBaseURL(workspace)
|
||||
this.anthropicClient = new Anthropic({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
|
||||
getOpenaiClient() {
|
||||
if (!this.openaiClient) {
|
||||
throw new Error('OpenAI not initialized')
|
||||
}
|
||||
return this.openaiClient
|
||||
}
|
||||
|
||||
getAnthropicClient() {
|
||||
if (!this.anthropicClient) {
|
||||
throw new Error('Anthropic not initialized')
|
||||
}
|
||||
return this.anthropicClient
|
||||
}
|
||||
}
|
||||
|
||||
export const workspaceAIClients = new WorkspacedAIClients()
|
||||
@@ -460,7 +483,7 @@ const PROMPTS_CONFIGS = {
|
||||
gen: GEN_CONFIG
|
||||
}
|
||||
|
||||
function getProviderAndCompletionConfig<K extends boolean>({
|
||||
export function getProviderAndCompletionConfig<K extends boolean>({
|
||||
messages,
|
||||
stream,
|
||||
tools,
|
||||
@@ -622,10 +645,11 @@ export async function getCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
tools?: OpenAI.Chat.Completions.ChatCompletionTool[]
|
||||
) {
|
||||
): Promise<Stream<ChatCompletionChunk>> {
|
||||
const { provider, config } = getProviderAndCompletionConfig({ messages, stream: true, tools })
|
||||
|
||||
const openaiClient = workspaceAIClients.getOpenaiClient()
|
||||
const completion = await openaiClient.chat.completions.create(config, {
|
||||
const completion = openaiClient.chat.completions.create(config, {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'X-Provider': provider
|
||||
@@ -634,6 +658,108 @@ export async function getCompletion(
|
||||
return completion
|
||||
}
|
||||
|
||||
export async function parseOpenAICompletion(
|
||||
completion: Stream<ChatCompletionChunk>,
|
||||
callbacks: ToolCallbacks & {
|
||||
onNewToken: (token: string) => void
|
||||
onMessageEnd: () => void
|
||||
},
|
||||
messages: ChatCompletionMessageParam[],
|
||||
addedMessages: ChatCompletionMessageParam[],
|
||||
tools: Tool<any>[],
|
||||
helpers: any
|
||||
): Promise<boolean> {
|
||||
const finalToolCalls: Record<number, ChatCompletionChunk.Choice.Delta.ToolCall> = {}
|
||||
|
||||
let answer = ''
|
||||
for await (const chunk of completion) {
|
||||
if (!('choices' in chunk && chunk.choices.length > 0 && 'delta' in chunk.choices[0])) {
|
||||
continue
|
||||
}
|
||||
const c = chunk as ChatCompletionChunk
|
||||
const delta = c.choices[0].delta.content
|
||||
if (delta) {
|
||||
answer += delta
|
||||
callbacks.onNewToken(delta)
|
||||
}
|
||||
const toolCalls = c.choices[0].delta.tool_calls || []
|
||||
if (toolCalls.length > 0 && answer) {
|
||||
// if tool calls are present but we have some textual content already, we need to display it to the user first
|
||||
callbacks.onMessageEnd()
|
||||
answer = ''
|
||||
}
|
||||
for (const toolCall of toolCalls) {
|
||||
const { index } = toolCall
|
||||
let finalToolCall = finalToolCalls[index]
|
||||
if (!finalToolCall) {
|
||||
finalToolCalls[index] = toolCall
|
||||
} else {
|
||||
if (toolCall.function?.arguments) {
|
||||
if (!finalToolCall.function) {
|
||||
finalToolCall.function = toolCall.function
|
||||
} else {
|
||||
finalToolCall.function.arguments =
|
||||
(finalToolCall.function.arguments ?? '') + toolCall.function.arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
finalToolCall = finalToolCalls[index]
|
||||
if (finalToolCall?.function) {
|
||||
const {
|
||||
function: { name: funcName },
|
||||
id: toolCallId
|
||||
} = finalToolCall
|
||||
if (funcName && toolCallId) {
|
||||
const tool = tools.find((t) => t.def.function.name === funcName)
|
||||
if (tool && tool.preAction) {
|
||||
tool.preAction({ toolCallbacks: callbacks, toolId: toolCallId })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (answer) {
|
||||
const toAdd = { role: 'assistant' as const, content: answer }
|
||||
addedMessages.push(toAdd)
|
||||
messages.push(toAdd)
|
||||
}
|
||||
|
||||
callbacks.onMessageEnd()
|
||||
|
||||
const toolCalls = Object.values(finalToolCalls).filter(
|
||||
(toolCall) => toolCall.id !== undefined && toolCall.function?.arguments !== undefined
|
||||
) as ChatCompletionMessageFunctionToolCall[]
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const toAdd = {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: toolCalls.map((t) => ({
|
||||
...t,
|
||||
function: {
|
||||
...t.function,
|
||||
arguments: t.function.arguments || '{}'
|
||||
}
|
||||
}))
|
||||
}
|
||||
messages.push(toAdd)
|
||||
addedMessages.push(toAdd)
|
||||
for (const toolCall of toolCalls) {
|
||||
const messageToAdd = await processToolCall({
|
||||
tools,
|
||||
toolCall,
|
||||
helpers,
|
||||
toolCallbacks: callbacks
|
||||
})
|
||||
messages.push(messageToAdd)
|
||||
addedMessages.push(messageToAdd)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function getResponseFromEvent(part: OpenAI.Chat.Completions.ChatCompletionChunk): string {
|
||||
return part.choices?.[0]?.delta?.content || ''
|
||||
}
|
||||
@@ -757,7 +883,7 @@ export async function deltaCodeCompletion(
|
||||
}
|
||||
|
||||
if (!match[1].endsWith('`')) {
|
||||
// skip udpating if possible that part of three ticks (end of code block)s
|
||||
// skip updating if possible that part of three ticks (end of code block)s
|
||||
delta = getStringEndDelta(code, match[1])
|
||||
generatedCodeDelta.set(delta)
|
||||
code = match[1]
|
||||
|
||||
@@ -40,9 +40,10 @@
|
||||
} from '$lib/components/schema/schemaUtils.svelte'
|
||||
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
import { isObjectTooBig } from '$lib/utils'
|
||||
import { isObjectTooBig, readFieldsRecursively } from '$lib/utils'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import type { ScriptLang } from '$lib/gen'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
@@ -333,11 +334,19 @@
|
||||
|
||||
function resetArgs() {
|
||||
if (!previewSchema) {
|
||||
// previewArguments = undefined
|
||||
savedPreviewArgs = undefined
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!previewArgs && savedPreviewArgs != undefined) {
|
||||
readFieldsRecursively(flowStore.val.schema)
|
||||
untrack(() => {
|
||||
resetArgs()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let historicInputs: HistoricInputs | undefined = $state(undefined)
|
||||
let captureTable: CaptureTable | undefined = $state(undefined)
|
||||
let savedInputsPicker: SavedInputsPicker | undefined = $state(undefined)
|
||||
@@ -363,9 +372,6 @@
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema={flowStore.val.schema}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addPropertyV2?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addPropertyV2?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
@@ -399,14 +405,13 @@
|
||||
})
|
||||
}}
|
||||
shouldDispatchChanges={true}
|
||||
on:change={() => {
|
||||
onChange={() => {
|
||||
if (!previewSchema) {
|
||||
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
|
||||
let args = $state.snapshot(previewArgs.val)
|
||||
if (!deepEqual(args, savedPreviewArgs)) {
|
||||
savedPreviewArgs = args
|
||||
}
|
||||
}
|
||||
refreshStateStore(flowStore)
|
||||
}}
|
||||
on:schemaChange={() => {
|
||||
resetArgs()
|
||||
}}
|
||||
bind:isValid
|
||||
bind:dynSelectCode
|
||||
@@ -475,15 +480,9 @@
|
||||
<AddPropertyV2
|
||||
bind:schema={flowStore.val.schema}
|
||||
bind:this={addPropertyV2}
|
||||
on:change={() => {
|
||||
refreshStateStore(flowStore)
|
||||
if (editableSchemaForm) {
|
||||
editableSchemaForm.updateJson()
|
||||
}
|
||||
}}
|
||||
on:addNew={(e) => {
|
||||
onAddNew={(argName) => {
|
||||
handleEditSchema('inputEditor')
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
editableSchemaForm?.openField(argName)
|
||||
refreshStateStore(flowStore)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import InputTransformForm from '$lib/components/InputTransformForm.svelte'
|
||||
@@ -49,7 +47,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
run(() => {
|
||||
$effect(() => {
|
||||
if ($workspaceStore && allUserGroups.length === 0) {
|
||||
untrack(() => {
|
||||
loadGroups()
|
||||
@@ -57,9 +55,22 @@
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
// If the schema is empty, remove the form
|
||||
if (Object.keys(flowModule?.suspend?.resume_form?.schema?.properties ?? {}).length === 0) {
|
||||
untrack(() => {
|
||||
tick().then(() => {
|
||||
if (!flowModule.suspend) return
|
||||
flowModule.suspend.resume_form = undefined
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let jsonView: boolean = $state(false)
|
||||
</script>
|
||||
|
||||
A{JSON.stringify(flowModule.suspend?.resume_form)}B
|
||||
<Section label="Suspend/Approval/Prompt" class="w-full">
|
||||
{#snippet action()}
|
||||
<SuspendDrawer text="Approval/Prompt helpers" />
|
||||
@@ -234,21 +245,7 @@
|
||||
<div class="grid grid-cols-4 mt-4 gap-8">
|
||||
<div class="col-span-2">
|
||||
{#if flowModule?.suspend?.resume_form}
|
||||
<EditableSchemaDrawer
|
||||
bind:schema={flowModule.suspend.resume_form.schema}
|
||||
on:change={(e) => {
|
||||
const schema = e.detail
|
||||
|
||||
// If the schema is empty, remove the form
|
||||
if (Object.keys(schema?.properties ?? {}).length === 0) {
|
||||
tick().then(() => {
|
||||
if (!flowModule.suspend) return
|
||||
flowModule.suspend.resume_form = undefined
|
||||
})
|
||||
}
|
||||
}}
|
||||
{jsonView}
|
||||
/>
|
||||
<EditableSchemaDrawer bind:schema={flowModule.suspend.resume_form.schema} {jsonView} />
|
||||
{:else if emptyString($enterpriseLicense)}
|
||||
<Alert type="warning" title="Adding a form to the approval page is an EE feature" />
|
||||
{:else}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
export async function openDrawer(hash: string, cb: () => void): Promise<void> {
|
||||
script = undefined
|
||||
closeAnyway = false
|
||||
scriptEditorDrawer?.openDrawer?.()
|
||||
script = await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -114,7 +115,6 @@
|
||||
async function checkForUnsavedChanges() {
|
||||
if (closeAnyway) {
|
||||
scriptEditorDrawer?.closeDrawer()
|
||||
closeAnyway = false
|
||||
return
|
||||
}
|
||||
if (savedScript && script) {
|
||||
@@ -122,12 +122,15 @@
|
||||
const current = cleanValueProperties(script)
|
||||
if (orderedJsonStringify(saved) !== orderedJsonStringify(current)) {
|
||||
unsavedModalOpen = true
|
||||
scriptEditorDrawer?.openDrawer()
|
||||
} else {
|
||||
scriptEditorDrawer?.closeDrawer()
|
||||
}
|
||||
}
|
||||
}
|
||||
let args = $state({})
|
||||
|
||||
let displayEditor = $state(true)
|
||||
</script>
|
||||
|
||||
<ConfirmationModal
|
||||
@@ -138,8 +141,9 @@
|
||||
unsavedModalOpen = false
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
unsavedModalOpen = false
|
||||
console.log('confirmed')
|
||||
closeAnyway = true
|
||||
unsavedModalOpen = false
|
||||
scriptEditorDrawer?.closeDrawer()
|
||||
}}
|
||||
>
|
||||
@@ -156,7 +160,7 @@
|
||||
}
|
||||
unsavedModalOpen = false
|
||||
closeAnyway = true
|
||||
scriptEditorDrawer?.closeDrawer()
|
||||
displayEditor = false
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
title: 'Saved <> Current',
|
||||
@@ -182,7 +186,6 @@
|
||||
bind:this={scriptEditorDrawer}
|
||||
size="1200px"
|
||||
on:close={() => {
|
||||
scriptEditorDrawer?.openDrawer()
|
||||
checkForUnsavedChanges()
|
||||
}}
|
||||
>
|
||||
@@ -195,7 +198,7 @@
|
||||
scriptEditorDrawer?.closeDrawer()
|
||||
}}
|
||||
>
|
||||
{#if script}
|
||||
{#if script && displayEditor}
|
||||
{#key script.hash}
|
||||
<ScriptEditor
|
||||
showCaptures={false}
|
||||
@@ -236,7 +239,7 @@
|
||||
return
|
||||
}
|
||||
closeAnyway = true
|
||||
scriptEditorDrawer?.closeDrawer()
|
||||
displayEditor = false
|
||||
diffDrawer?.openDrawer()
|
||||
diffDrawer?.setDiff({
|
||||
mode: 'simple',
|
||||
@@ -276,10 +279,6 @@
|
||||
<DiffDrawer
|
||||
bind:this={diffDrawer}
|
||||
on:close={() => {
|
||||
if (!closeAnyway) {
|
||||
scriptEditorDrawer?.openDrawer()
|
||||
} else {
|
||||
closeAnyway = false
|
||||
}
|
||||
displayEditor = true
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -487,7 +487,7 @@
|
||||
additionalAssetsMap: flowGraphAssetsCtx?.val.additionalAssetsMap
|
||||
},
|
||||
untrack(() => failureModule),
|
||||
untrack(() => preprocessorModule),
|
||||
preprocessorModule,
|
||||
eventHandler,
|
||||
success,
|
||||
$useDataflow,
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px' }: Props = $props();
|
||||
let { height, width, size = 24 }: Props = $props()
|
||||
|
||||
let derivedWidth = $derived(width || `${size}px`)
|
||||
let derivedHeight = $derived(height || `${size}px`)
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill="#F05133"
|
||||
d="M92.71 44.408 52.591 4.291c-2.31-2.311-6.057-2.311-8.369 0l-8.33 8.332L46.459 23.19c2.456-.83 5.272-.273 7.229 1.685 1.969 1.97 2.521 4.81 1.67 7.275l10.186 10.185c2.465-.85 5.307-.3 7.275 1.671 2.75 2.75 2.75 7.206 0 9.958-2.752 2.751-7.208 2.751-9.961 0-2.068-2.07-2.58-5.11-1.531-7.658l-9.5-9.499v24.997c.67.332 1.303.774 1.861 1.332 2.75 2.75 2.75 7.206 0 9.959-2.75 2.749-7.209 2.749-9.957 0-2.75-2.754-2.75-7.21 0-9.959.68-.679 1.467-1.193 2.307-1.537v-25.23c-.84-.344-1.625-.853-2.307-1.537-2.083-2.082-2.584-5.14-1.516-7.698L31.798 16.715 4.288 44.222c-2.311 2.313-2.311 6.06 0 8.371l40.121 40.118c2.31 2.311 6.056 2.311 8.369 0L92.71 52.779c2.311-2.311 2.311-6.06 0-8.371z"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import { X, Minimize2, Maximize2 } from 'lucide-svelte'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import { pointerDownOutside } from '$lib/utils'
|
||||
import { debounce, pointerDownOutside } from '$lib/utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
@@ -24,6 +24,7 @@
|
||||
export let placement: Placement = 'bottom'
|
||||
export let disablePopup: boolean = false
|
||||
export let openOnHover: boolean = false
|
||||
export let debounceDelay: number = 0
|
||||
export let floatingConfig: any | undefined = undefined
|
||||
export let usePointerDownOutside: boolean = false
|
||||
export let closeOnOutsideClick: boolean = true
|
||||
@@ -41,6 +42,14 @@
|
||||
let fullScreen = false
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function clearTimers() {
|
||||
clearDebounceClose()
|
||||
}
|
||||
|
||||
// Cleanup timers on component destruction
|
||||
import { onDestroy } from 'svelte'
|
||||
onDestroy(clearTimers)
|
||||
|
||||
const {
|
||||
elements: { trigger, content, arrow, close: closeElement, overlay },
|
||||
states,
|
||||
@@ -109,6 +118,11 @@
|
||||
async function getMenuElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(document.querySelectorAll('[data-popover]')) as HTMLElement[]
|
||||
}
|
||||
|
||||
let { debounced: debounceClose, clearDebounce: clearDebounceClose } = debounce(
|
||||
() => openOnHover && close(),
|
||||
debounceDelay
|
||||
)
|
||||
</script>
|
||||
|
||||
<button
|
||||
@@ -116,8 +130,13 @@
|
||||
use:melt={$trigger}
|
||||
aria-label="Popup button"
|
||||
disabled={disablePopup || disabled}
|
||||
on:mouseenter={() => (openOnHover ? open() : null)}
|
||||
on:mouseleave={() => (openOnHover ? close() : null)}
|
||||
on:mouseenter={() => {
|
||||
if (openOnHover) {
|
||||
open()
|
||||
clearDebounceClose()
|
||||
}
|
||||
}}
|
||||
on:mouseleave={debounceClose}
|
||||
use:pointerDownOutside={{
|
||||
capture: true,
|
||||
stopPropagation: false,
|
||||
@@ -141,6 +160,13 @@
|
||||
|
||||
{#if isOpen && !disablePopup}
|
||||
<div
|
||||
on:mouseenter={() => {
|
||||
if (openOnHover) {
|
||||
open()
|
||||
clearDebounceClose()
|
||||
}
|
||||
}}
|
||||
on:mouseleave={debounceClose}
|
||||
use:melt={$content}
|
||||
transition:fade={{ duration: 0 }}
|
||||
class={twMerge(
|
||||
|
||||
@@ -83,6 +83,13 @@
|
||||
if (Object.keys(arg).length === 1 && Object.keys(arg).includes('s3')) {
|
||||
return 's3object'
|
||||
}
|
||||
if (
|
||||
Object.keys(arg).length === 2 &&
|
||||
Object.keys(arg).includes('s3') &&
|
||||
Object.keys(arg).includes('filename')
|
||||
) {
|
||||
return 's3object'
|
||||
}
|
||||
return typeof arg
|
||||
}
|
||||
|
||||
@@ -172,9 +179,12 @@
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
<Portal name="object-viewer">
|
||||
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
|
||||
</Portal>
|
||||
{#if getTypeAsString(jsonFiltered) === 's3object'}
|
||||
<Portal name="object-viewer">
|
||||
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
|
||||
</Portal>
|
||||
{/if}
|
||||
|
||||
{#if level == 0}
|
||||
<div class="float-right">
|
||||
{#if searchOpen}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import Popover from '../Popover.svelte'
|
||||
import { isFlowPreview, isScriptPreview, truncateRev } from '$lib/utils'
|
||||
import { createEventDispatcher, setContext, untrack } from 'svelte'
|
||||
import { ListFilter } from 'lucide-svelte'
|
||||
import { ListFilter, LoaderCircle } from 'lucide-svelte'
|
||||
import FlowAssetsHandler, { initFlowGraphAssetsCtx } from '../flows/FlowAssetsHandler.svelte'
|
||||
import JobAssetsViewer from '../assets/JobAssetsViewer.svelte'
|
||||
|
||||
@@ -78,199 +78,244 @@
|
||||
})
|
||||
|
||||
let jobLoader: JobLoader | undefined = $state(undefined)
|
||||
|
||||
// Set all tabs content to the same height to prevent layout jumps
|
||||
let tabsHeight = $state({
|
||||
codeHeight: 0,
|
||||
logsHeight: 0,
|
||||
assetsHeight: 0,
|
||||
resultHeight: 0
|
||||
})
|
||||
|
||||
let minTabHeight = $derived(
|
||||
Math.max(
|
||||
tabsHeight.codeHeight,
|
||||
tabsHeight.logsHeight,
|
||||
tabsHeight.assetsHeight,
|
||||
tabsHeight.resultHeight
|
||||
)
|
||||
)
|
||||
|
||||
let jobIsLoading = $state(false)
|
||||
</script>
|
||||
|
||||
<JobLoader workspaceOverride={workspace} bind:job={currentJob} bind:this={jobLoader} />
|
||||
<JobLoader
|
||||
workspaceOverride={workspace}
|
||||
bind:job={currentJob}
|
||||
bind:isLoading={jobIsLoading}
|
||||
bind:this={jobLoader}
|
||||
/>
|
||||
|
||||
<div class="p-4 flex flex-col gap-2 items-start h-full">
|
||||
{#if job}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{#if job?.['priority']}
|
||||
<Badge color="red">
|
||||
priority: {job?.['priority']}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<DurationMs
|
||||
duration_ms={job.duration_ms}
|
||||
self_wait_time_ms={job?.self_wait_time_ms}
|
||||
aggregate_wait_time_ms={job?.aggregate_wait_time_ms}
|
||||
/>
|
||||
{/if}
|
||||
{#if job?.['mem_peak']}
|
||||
<Badge large>
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if workspace && $workspaceStore != workspace}
|
||||
<Badge large>
|
||||
Workspace: {workspace}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job.tag}
|
||||
<Badge large>
|
||||
Tag: {job.tag}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0}
|
||||
{#each job?.['labels'] as label}
|
||||
<Badge baseClass="text-2xs">Label: {label}</Badge>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if concurrencyKey}
|
||||
<Popover notClickable>
|
||||
{#snippet text()}
|
||||
This job has concurrency limits enabled with the key:
|
||||
<Button
|
||||
class="inline-text"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByConcurrencyKey', concurrencyKey)
|
||||
}}
|
||||
>
|
||||
{concurrencyKey}
|
||||
<ListFilter class="inline-block" size={10} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
<Badge large>Concurrency: {truncateRev(concurrencyKey, 20)}</Badge>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if job?.worker}
|
||||
<Popover notClickable>
|
||||
{#snippet text()}
|
||||
This job was run on worker:
|
||||
<Button
|
||||
class="inline-text"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByWorker', job?.worker)
|
||||
}}
|
||||
>
|
||||
{job?.worker}
|
||||
<ListFilter class="inline-block" size={10} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
<Badge large>Worker: {truncateRev(job.worker, 20)}</Badge>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
href="{base}/run/{job?.id}?workspace={job?.workspace_id}"
|
||||
class="flex flex-row gap-1 items-center"
|
||||
target={blankLink ? '_blank' : undefined}
|
||||
>
|
||||
<span class="font-semibold text-sm leading-6">ID:</span>
|
||||
<span class="text-sm">{job?.id ?? ''}</span>
|
||||
</a>
|
||||
|
||||
<span class="font-semibold text-xs leading-6">Arguments</span>
|
||||
|
||||
<div class="w-full">
|
||||
<JobArgs
|
||||
id={job?.id}
|
||||
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job?.args}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if job?.type === 'CompletedJob'}
|
||||
<span class="font-semibold text-xs leading-6">Results</span>
|
||||
{/if}
|
||||
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1">
|
||||
<div>Job is scheduled for</div>
|
||||
<div>{new Date(job?.['scheduled_for']).toLocaleString()}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" w-full rounded-md min-h-full">
|
||||
{#if job?.workflow_as_code_status}
|
||||
<WorkflowTimeline
|
||||
flow_status={asWorkflowStatus(job.workflow_as_code_status)}
|
||||
flowDone={job.type == 'CompletedJob'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if job?.type === 'CompletedJob'}
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tab size="xs" value="result">Result</Tab>
|
||||
<Tab size="xs" value="logs">Logs</Tab>
|
||||
<Tab size="xs" value="assets">Assets</Tab>
|
||||
{#if isScriptPreview(job?.job_kind)}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
|
||||
<Skeleton loading={!job} layout={[[5]]} />
|
||||
{#if job}
|
||||
{#if viewTab == 'result' && (job?.job_kind == 'flow' || isFlowPreview(job?.job_kind))}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-full mt-10 mb-20">
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if viewTab == 'assets'}
|
||||
<JobAssetsViewer {job} />
|
||||
{:else}
|
||||
<div class="flex flex-col border rounded-md p-2 mt-2 h-full overflow-auto">
|
||||
{#if viewTab == 'logs'}
|
||||
<div class="w-full">
|
||||
<LogViewer
|
||||
jobId={job.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
isLoading={job?.['running'] == false}
|
||||
content={job?.logs}
|
||||
tag={job?.tag}
|
||||
/>
|
||||
</div>
|
||||
{:else if viewTab == 'code'}
|
||||
{#if job && 'raw_code' in job && job.raw_code}
|
||||
<div class="text-xs">
|
||||
<HighlightCode lines language={job.language} code={job.raw_code} />
|
||||
</div>
|
||||
{:else if job}
|
||||
No code is available
|
||||
{:else}
|
||||
<Skeleton layout={[[5]]} />
|
||||
{/if}
|
||||
{:else if job !== undefined && (job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined))}
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
{result}
|
||||
disableExpand
|
||||
language={job?.language}
|
||||
/>
|
||||
{:else if job}
|
||||
No output is available yet
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-2 items-start p-4 pb-8 min-h-full">
|
||||
{#if job}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{#if job?.['priority']}
|
||||
<Badge color="red">
|
||||
priority: {job?.['priority']}
|
||||
</Badge>
|
||||
{/if}
|
||||
{:else if job && `running` in job ? job.running : false}
|
||||
{#if job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<FlowProgressBar {job} class="py-4" />
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1"> Job is still running </div>
|
||||
<LogViewer
|
||||
jobId={job?.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
content={job?.logs}
|
||||
isLoading={job?.['running'] == false}
|
||||
tag={job?.tag}
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
<DurationMs
|
||||
duration_ms={job.duration_ms}
|
||||
self_wait_time_ms={job?.self_wait_time_ms}
|
||||
aggregate_wait_time_ms={job?.aggregate_wait_time_ms}
|
||||
/>
|
||||
{/if}
|
||||
{#if job?.['mem_peak']}
|
||||
<Badge large>
|
||||
Mem: {job?.['mem_peak'] ? `${(job['mem_peak'] / 1024).toPrecision(4)}MB` : 'N/A'}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if workspace && $workspaceStore != workspace}
|
||||
<Badge large>
|
||||
Workspace: {workspace}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job.tag}
|
||||
<Badge large>
|
||||
Tag: {job.tag}
|
||||
</Badge>
|
||||
{/if}
|
||||
{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0}
|
||||
{#each job?.['labels'] as label}
|
||||
<Badge baseClass="text-2xs">Label: {label}</Badge>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if concurrencyKey}
|
||||
<Popover notClickable>
|
||||
{#snippet text()}
|
||||
This job has concurrency limits enabled with the key:
|
||||
<Button
|
||||
class="inline-text"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByConcurrencyKey', concurrencyKey)
|
||||
}}
|
||||
>
|
||||
{concurrencyKey}
|
||||
<ListFilter class="inline-block" size={10} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
<Badge large>Concurrency: {truncateRev(concurrencyKey, 20)}</Badge>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if job?.worker}
|
||||
<Popover notClickable>
|
||||
{#snippet text()}
|
||||
This job was run on worker:
|
||||
<Button
|
||||
class="inline-text"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByWorker', job?.worker)
|
||||
}}
|
||||
>
|
||||
{job?.worker}
|
||||
<ListFilter class="inline-block" size={10} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
<Badge large>Worker: {truncateRev(job.worker, 20)}</Badge>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<a
|
||||
href="{base}/run/{job?.id}?workspace={job?.workspace_id}"
|
||||
class="flex flex-row gap-1 items-center"
|
||||
target={blankLink ? '_blank' : undefined}
|
||||
>
|
||||
<span class="font-semibold text-sm leading-6">ID:</span>
|
||||
<span class="text-sm">{job?.id ?? ''}</span>
|
||||
</a>
|
||||
|
||||
<div class="w-full">
|
||||
<JobArgs
|
||||
id={job?.id}
|
||||
workspace={job?.workspace_id ?? $workspaceStore ?? 'no_w'}
|
||||
args={job?.args}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if job && 'scheduled_for' in job && !job.running && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1">
|
||||
<div>Job is scheduled for</div>
|
||||
<div>{new Date(job?.['scheduled_for']).toLocaleString()}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full rounded-md min-h-full">
|
||||
{#if job?.workflow_as_code_status}
|
||||
<WorkflowTimeline
|
||||
flow_status={asWorkflowStatus(job.workflow_as_code_status)}
|
||||
flowDone={job.type == 'CompletedJob'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if job?.type === 'CompletedJob'}
|
||||
{#if job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)}
|
||||
<div class="w-full mt-8 mb-20">
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} wideResults
|
||||
></FlowStatusViewer>
|
||||
</div>
|
||||
{:else}
|
||||
<Tabs bind:selected={viewTab}>
|
||||
<Tab size="xs" value="result">Results</Tab>
|
||||
<Tab size="xs" value="logs">Logs</Tab>
|
||||
<Tab size="xs" value="assets">Assets</Tab>
|
||||
{#if isScriptPreview(job?.job_kind)}
|
||||
<Tab size="xs" value="code">Code</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
|
||||
<Skeleton loading={!job} layout={[[5]]} />
|
||||
{#if job}
|
||||
<div class="flex flex-col border rounded-md p-2 mt-2 overflow-auto">
|
||||
{#if viewTab == 'logs'}
|
||||
<div
|
||||
class="w-full"
|
||||
bind:clientHeight={tabsHeight.logsHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
<LogViewer
|
||||
jobId={job.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
isLoading={job?.['running'] == false}
|
||||
content={job?.logs}
|
||||
tag={job?.tag}
|
||||
/>
|
||||
</div>
|
||||
{:else if viewTab == 'assets'}
|
||||
<div
|
||||
class="w-full h-full"
|
||||
bind:clientHeight={tabsHeight.assetsHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
<JobAssetsViewer {job} />
|
||||
</div>
|
||||
{:else if viewTab == 'code'}
|
||||
<div
|
||||
class="text-xs"
|
||||
bind:clientHeight={tabsHeight.codeHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
{#if job && 'raw_code' in job && job.raw_code}
|
||||
<div class="text-xs">
|
||||
<HighlightCode lines language={job.language} code={job.raw_code} />
|
||||
</div>
|
||||
{:else if job}
|
||||
<span class="text-sm">No code available</span>
|
||||
{:else}
|
||||
<Skeleton layout={[[5]]} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if job !== undefined && (job.result_stream || (job.type == 'CompletedJob' && job.result !== undefined))}
|
||||
<div
|
||||
class="w-full"
|
||||
bind:clientHeight={tabsHeight.resultHeight}
|
||||
style="min-height: {minTabHeight}px"
|
||||
>
|
||||
<DisplayResult
|
||||
workspaceId={job?.workspace_id}
|
||||
jobId={job?.id}
|
||||
{result}
|
||||
disableExpand
|
||||
language={job?.language}
|
||||
/>
|
||||
</div>
|
||||
{:else if job}
|
||||
No output is available yet
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if job && `running` in job ? job.running : false}
|
||||
{#if job?.job_kind == 'flow' || isFlowPreview(job?.job_kind)}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<FlowProgressBar {job} class="py-4" />
|
||||
<FlowStatusViewer jobId={job.id} workspaceId={job.workspace_id} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm font-semibold text-tertiary mb-1"> Job is still running </div>
|
||||
<LogViewer
|
||||
jobId={job?.id}
|
||||
duration={job?.['duration_ms']}
|
||||
mem={job?.['mem_peak']}
|
||||
content={job?.logs}
|
||||
isLoading={job?.['running'] == false}
|
||||
tag={job?.tag}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if jobIsLoading}
|
||||
<div class="mx-auto my-auto">
|
||||
<LoaderCircle size={20} class="animate-spin" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<FlowAssetsHandler
|
||||
modules={job?.raw_flow?.modules ?? []}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from '$lib/components/meltComponents/Tooltip.svelte'
|
||||
import PreprocessedArgsDisplay from '$lib/components/runs/PreprocessedArgsDisplay.svelte'
|
||||
import { truncateHash } from '$lib/utils'
|
||||
import { base } from '$app/paths'
|
||||
import { truncateRev } from '$lib/utils'
|
||||
import WorkerHostname from '$lib/components/WorkerHostname.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
job: Job
|
||||
displayPersistentScriptDefinition?: boolean
|
||||
openPersistentScriptDrawer?: () => void
|
||||
concurrencyKey?: string
|
||||
showScriptHash?: boolean
|
||||
verySmall?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
job,
|
||||
displayPersistentScriptDefinition,
|
||||
openPersistentScriptDrawer,
|
||||
concurrencyKey,
|
||||
showScriptHash = true,
|
||||
verySmall = false
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if job.script_hash && showScriptHash && job.job_kind !== 'aiagent'}
|
||||
{#if job.job_kind == 'script'}
|
||||
<a href="{base}/scripts/get/{job.script_hash}?workspace={$workspaceStore}"
|
||||
><Badge color="gray" {verySmall}>{truncateHash(job.script_hash)}</Badge></a
|
||||
>
|
||||
{:else}
|
||||
<div>
|
||||
<Badge color="gray" {verySmall}>{truncateHash(job.script_hash)}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if job && 'job_kind' in job}
|
||||
<div>
|
||||
<Badge color="blue" {verySmall}>{job.job_kind}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job && job.flow_status && job.job_kind === 'script'}
|
||||
<PreprocessedArgsDisplay preprocessed={job.preprocessed} />
|
||||
{/if}
|
||||
{#if displayPersistentScriptDefinition}
|
||||
<button onclick={() => openPersistentScriptDrawer?.()}>
|
||||
<Badge color="red">persistent</Badge>
|
||||
</button>
|
||||
{/if}
|
||||
{#if job && 'priority' in job}
|
||||
<div>
|
||||
<Badge color="blue" {verySmall}>priority: {job.priority}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job.tag}
|
||||
<!-- for related places search: ADD_NEW_LANG -->
|
||||
<div>
|
||||
<Badge color="indigo" {verySmall}>Tag: {job.tag}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !job.visible_to_owner}
|
||||
<div>
|
||||
<Badge color="red" {verySmall}>
|
||||
only visible to you
|
||||
<Tooltip>
|
||||
{#snippet text()}
|
||||
The option to hide this run from the owner of this script or flow was activated
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job?.['labels'] && Array.isArray(job?.['labels']) && job?.['labels'].length > 0}
|
||||
{#each job?.['labels'] as label}
|
||||
<div>
|
||||
<Badge {verySmall}>Label: {label}</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if concurrencyKey}
|
||||
<div>
|
||||
<Tooltip notClickable>
|
||||
{#snippet text()}
|
||||
This job has concurrency limits enabled with the key
|
||||
<a
|
||||
href={`${base}/runs/?job_kinds=all&graph=ConcurrencyChart&concurrency_key=${concurrencyKey}`}
|
||||
>
|
||||
{concurrencyKey}
|
||||
</a>
|
||||
{/snippet}
|
||||
<a
|
||||
href={`${base}/runs/?job_kinds=all&graph=ConcurrencyChart&concurrency_key=${concurrencyKey}`}
|
||||
>
|
||||
<Badge {verySmall}>Concurrency: {truncateRev(concurrencyKey, 20)}</Badge></a
|
||||
>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job?.worker}
|
||||
<div>
|
||||
<Tooltip notClickable>
|
||||
{#snippet text()}
|
||||
worker:
|
||||
<a href={`${base}/runs/?job_kinds=all&worker=${job?.worker}`}>
|
||||
{job?.worker}
|
||||
</a><br />
|
||||
<WorkerHostname worker={job?.worker!} minTs={job?.['created_at']} />
|
||||
{/snippet}
|
||||
<a href={`${base}/runs/?job_kinds=all&worker=${job?.worker}`}>
|
||||
<Badge {verySmall}>Worker: {truncateRev(job?.worker, 20)}</Badge></a
|
||||
>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,112 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { ListFilterPlus } from 'lucide-svelte'
|
||||
import Tooltip from '$lib/components/meltComponents/Tooltip.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import type { Item } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
job: Job
|
||||
activeLabel: string | null
|
||||
onFilterByLabel: (label: string) => void
|
||||
labelWidth: number
|
||||
}
|
||||
|
||||
let { job, activeLabel, onFilterByLabel, labelWidth }: Props = $props()
|
||||
|
||||
const GAP = 4
|
||||
const LABEL_MAX_WIDTH = 84
|
||||
const MORE_LABEL_WIDTH = 30
|
||||
// Min width for labels columns is 120px
|
||||
|
||||
const labels = $derived(job && Array.isArray(job?.['labels']) ? (job['labels'] as string[]) : [])
|
||||
|
||||
const labelSplit = $derived.by(() => {
|
||||
if (!labels || labels.length === 0 || labelWidth <= 0) {
|
||||
return { visibleLabels: [], hiddenLabels: [] }
|
||||
}
|
||||
|
||||
if (labels.length === 1) {
|
||||
return { visibleLabels: labels, hiddenLabels: [] }
|
||||
}
|
||||
|
||||
let currentWidth = 0
|
||||
const visible: string[] = []
|
||||
const hidden: string[] = []
|
||||
const margin = 20
|
||||
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
const label = labels[i]
|
||||
|
||||
// Check if we need to reserve space for overflow badge
|
||||
const needsOverflowBadge = i < labels.length - 1
|
||||
const remainingWidth = labelWidth - currentWidth
|
||||
const requiredWidth =
|
||||
LABEL_MAX_WIDTH + (needsOverflowBadge ? MORE_LABEL_WIDTH + GAP : 0) + margin
|
||||
|
||||
if (remainingWidth >= requiredWidth || visible.length === 0) {
|
||||
visible.push(label)
|
||||
currentWidth += LABEL_MAX_WIDTH + GAP
|
||||
} else {
|
||||
hidden.push(...labels.slice(i))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { visibleLabels: visible, hiddenLabels: hidden }
|
||||
})
|
||||
|
||||
const visibleLabels = $derived(labelSplit.visibleLabels || [])
|
||||
const hiddenLabels = $derived(labelSplit.hiddenLabels || [])
|
||||
|
||||
const dropdownItems = $derived(
|
||||
hiddenLabels.map(
|
||||
(label): Item => ({
|
||||
displayName: label,
|
||||
action: () => onFilterByLabel(label),
|
||||
icon: ListFilterPlus
|
||||
})
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if labels && labels.length > 0}
|
||||
<div class="flex flex-row items-center" style="gap: {GAP}px">
|
||||
{#each visibleLabels as label}
|
||||
<Tooltip openDelay={500} placement="bottom">
|
||||
<button
|
||||
class={twMerge(
|
||||
activeLabel == label ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'flex flex-row items-center px-2 group py-1 rounded-md bg-surface-secondary hover:bg-surface'
|
||||
)}
|
||||
style="gap: {GAP}px; width: {LABEL_MAX_WIDTH}px"
|
||||
onclick={() => {
|
||||
onFilterByLabel(label)
|
||||
}}
|
||||
>
|
||||
<span class="truncate text-2xs font-normal">{label}</span>
|
||||
<ListFilterPlus size={12} class="shrink-0 text-gray-300 group-hover:text-primary" />
|
||||
</button>
|
||||
{#snippet text()}
|
||||
{`Filter by label: ${label}`}
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
{/each}
|
||||
|
||||
{#if hiddenLabels.length > 0}
|
||||
<DropdownV2 placement="bottom-start" items={dropdownItems} customWidth={224}>
|
||||
{#snippet buttonReplacement()}
|
||||
<button
|
||||
class="flex flex-row items-center justify-center px-2 py-1 text-2xs font-semibold hover:bg-surface bg-surface-secondary text-secondary rounded-md"
|
||||
style="gap: {GAP}px; width: {MORE_LABEL_WIDTH}px"
|
||||
>
|
||||
+{hiddenLabels.length}
|
||||
</button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-2xs text-secondary">-</span>
|
||||
{/if}
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
children: import('svelte').Snippet
|
||||
tooltip?: import('svelte').Snippet
|
||||
for?: string
|
||||
noLabel?: boolean
|
||||
}
|
||||
|
||||
let { label, children, tooltip, for: forAttr = '', noLabel = false }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-start w-fit">
|
||||
<div class="text-xs truncate">
|
||||
{#if forAttr}
|
||||
<label for={forAttr} class:text-transparent={noLabel}>{label}</label>
|
||||
{:else}
|
||||
<span class:text-transparent={noLabel}>{label}</span>
|
||||
{/if}
|
||||
{#if tooltip}
|
||||
<Tooltip small>{@render tooltip()}</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-1 items-center justify-start w-full relative h-[34px]">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,24 +4,28 @@
|
||||
import type { Job } from '$lib/gen'
|
||||
import {
|
||||
displayDate,
|
||||
msToReadableTime,
|
||||
truncateHash,
|
||||
truncateRev,
|
||||
isFlowPreview,
|
||||
isScriptPreview,
|
||||
isJobSelectable
|
||||
isJobSelectable,
|
||||
msToReadableTime,
|
||||
isFlowPreview
|
||||
} from '$lib/utils'
|
||||
import { Badge, Button } from '../common'
|
||||
import ScheduleEditor from '$lib/components/triggers/schedules/ScheduleEditor.svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
|
||||
import {
|
||||
Bot,
|
||||
Calendar,
|
||||
Check,
|
||||
Clock,
|
||||
Code,
|
||||
ExternalLink,
|
||||
FastForward,
|
||||
Folder,
|
||||
Hourglass,
|
||||
ListFilter,
|
||||
ListFilterPlus,
|
||||
Package,
|
||||
Play,
|
||||
ShieldQuestion,
|
||||
X
|
||||
@@ -34,45 +38,104 @@
|
||||
|
||||
import WaitTimeWarning from '../common/waitTimeWarning/WaitTimeWarning.svelte'
|
||||
import type { RunsSelectionMode } from './RunsBatchActionsDropdown.svelte'
|
||||
import DropdownV2 from '../DropdownV2.svelte'
|
||||
import { Tooltip } from '../meltComponents'
|
||||
import { GitIcon } from '../icons'
|
||||
import RunLabels from './RunLabels.svelte'
|
||||
import './runs-grid.css'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let job: Job
|
||||
export let selected: boolean = false
|
||||
export let containerWidth: number = 0
|
||||
export let containsLabel: boolean = false
|
||||
export let activeLabel: string | null
|
||||
export let selectionMode: RunsSelectionMode | false = false
|
||||
interface Props {
|
||||
job: Job
|
||||
selected?: boolean
|
||||
containerWidth?: number
|
||||
containsLabel?: boolean
|
||||
showTag?: boolean
|
||||
activeLabel: string | null
|
||||
selectionMode?: RunsSelectionMode | false
|
||||
}
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
let {
|
||||
job,
|
||||
selected = false,
|
||||
containerWidth = 0,
|
||||
containsLabel = false,
|
||||
showTag = true,
|
||||
activeLabel,
|
||||
selectionMode = false
|
||||
}: Props = $props()
|
||||
|
||||
$: isExternal = job && job.id === '-'
|
||||
let scheduleEditor: ScheduleEditor | undefined = $state(undefined)
|
||||
|
||||
let isExternal = $derived(job && job.id === '-')
|
||||
|
||||
function getJobKindIcon(jobKind: Job['job_kind']) {
|
||||
if (jobKind === 'flow' || isFlowPreview(jobKind)) {
|
||||
return BarsStaggered
|
||||
} else if (jobKind === 'deploymentcallback') {
|
||||
return GitIcon
|
||||
} else if (
|
||||
jobKind === 'dependencies' ||
|
||||
jobKind === 'appdependencies' ||
|
||||
jobKind === 'flowdependencies'
|
||||
) {
|
||||
return Package
|
||||
} else if (
|
||||
jobKind === 'script' ||
|
||||
isScriptPreview(jobKind) ||
|
||||
jobKind === 'script_hub' ||
|
||||
jobKind === 'singlescriptflow'
|
||||
) {
|
||||
return Code
|
||||
} else if (jobKind === 'aiagent') {
|
||||
return Bot
|
||||
}
|
||||
return Code
|
||||
}
|
||||
|
||||
let labelWidth = $state(0)
|
||||
|
||||
let isJobRecent = $state(true)
|
||||
</script>
|
||||
|
||||
<Portal name="run-row">
|
||||
<ScheduleEditor onUpdate={() => goto('/schedules')} bind:this={scheduleEditor} />
|
||||
</Portal>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover cursor-pointer',
|
||||
selected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'flex flex-row items-center h-full'
|
||||
'grid items-center h-full'
|
||||
)}
|
||||
class:grid-runs-table={!containsLabel && !selectionMode && showTag}
|
||||
class:grid-runs-table-with-labels={containsLabel && !selectionMode && showTag}
|
||||
class:grid-runs-table-selection={!containsLabel && selectionMode && showTag}
|
||||
class:grid-runs-table-with-labels-selection={containsLabel && selectionMode && showTag}
|
||||
class:grid-runs-table-no-tag={!containsLabel && !selectionMode && !showTag}
|
||||
class:grid-runs-table-with-labels-no-tag={containsLabel && !selectionMode && !showTag}
|
||||
class:grid-runs-table-selection-no-tag={!containsLabel && selectionMode && !showTag}
|
||||
class:grid-runs-table-with-labels-selection-no-tag={containsLabel && selectionMode && !showTag}
|
||||
style="width: {containerWidth}px"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
if (!selectionMode || isJobSelectable(selectionMode)(job)) {
|
||||
dispatch('select')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="w-1/12 flex justify-center">
|
||||
{#if selectionMode && isJobSelectable(selectionMode)(job)}
|
||||
<div class="px-2">
|
||||
<input type="checkbox" checked={selected} />
|
||||
<!-- Selection column (only when in selection mode) -->
|
||||
{#if selectionMode}
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="w-4 h-4">
|
||||
<input type="checkbox" checked={selected} disabled={!isJobSelectable(selectionMode)(job)} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Status -->
|
||||
<div class="flex items-center justify-start pl-2">
|
||||
{#if isExternal}
|
||||
<Badge color="gray" baseClass="!px-1.5">
|
||||
<ShieldQuestion size={14} />
|
||||
@@ -114,16 +177,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="w-4/12 flex justify-start">
|
||||
<div class="flex flex-row items-center gap-1 text-gray-500 dark:text-gray-300 text-2xs">
|
||||
<!-- Job time -->
|
||||
<div class="overflow-hidden min-w-0">
|
||||
<div class="flex flex-row items-center gap-1 text-secondary text-2xs">
|
||||
{#if job}
|
||||
{#if 'started_at' in job && job.started_at}
|
||||
Started <TimeAgo agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
(Ran in {msToReadableTime(
|
||||
job.duration_ms
|
||||
)}{#if job.job_kind == 'flow' || isFlowPreview(job.job_kind)} total{/if})
|
||||
{/if}
|
||||
{isJobRecent ? 'Started' : ''}
|
||||
<TimeAgo bind:isRecent={isJobRecent} agoOnlyIfRecent date={job.started_at ?? ''} />
|
||||
{#if job && (job.self_wait_time_ms || job.aggregate_wait_time_ms)}
|
||||
<WaitTimeWarning
|
||||
self_wait_time_ms={job.self_wait_time_ms}
|
||||
@@ -132,7 +192,7 @@
|
||||
/>
|
||||
{/if}
|
||||
{:else if `scheduled_for` in job && job.scheduled_for && forLater(job.scheduled_for)}
|
||||
Scheduled for {displayDate(job.scheduled_for)}
|
||||
{displayDate(job.scheduled_for)}<Clock size={12} />
|
||||
{:else if job.canceled}
|
||||
{#if job.type == 'CompletedJob'}
|
||||
Cancelled <TimeAgo agoOnlyIfRecent date={job.created_at || ''} />
|
||||
@@ -151,154 +211,207 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-4/12 flex justify-start flex-col">
|
||||
<div class="flex flex-row text-sm">
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1 min-w-0">
|
||||
<div class="whitespace-nowrap text-xs font-semibold truncate">
|
||||
{#if job.script_path}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{#if isExternal}
|
||||
<span class="w-30 justify-center">-</span>
|
||||
{:else}
|
||||
<a
|
||||
href="{base}/run/{job.id}?workspace={job.workspace_id}"
|
||||
class="truncate w-30 dark:text-blue-400"
|
||||
>
|
||||
{job.script_path}
|
||||
</a>
|
||||
<Button
|
||||
title="Filter by path"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByPath', job.script_path)
|
||||
}}
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if job.script_path?.startsWith('f/')}
|
||||
<Button
|
||||
title="Filter by folder"
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
// split script_path by / and get the second element
|
||||
const folder = job.script_path?.split('/')[1]
|
||||
|
||||
dispatch('filterByFolder', folder)
|
||||
}}
|
||||
>
|
||||
<Folder size={10} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if 'job_kind' in job && isScriptPreview(job.job_kind)}
|
||||
<a href="{base}/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
|
||||
<a href="{base}/run/{job.id}?workspace={job.workspace_id}">
|
||||
lock deps of {truncateHash(job.script_hash ?? '')}
|
||||
</a>
|
||||
{:else if 'job_kind' in job && job.job_kind == 'identity'}
|
||||
<a href="{base}/run/{job.id}?workspace={job.workspace_id}">no op</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if job && job.parent_job}
|
||||
{#if job.is_flow_step}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<BarsStaggered class="text-secondary" size={14} />
|
||||
<span class="mx-1 text-xs">
|
||||
Step of flow <a href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id}`}>
|
||||
{truncateRev(job.parent_job, 6)}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<span class="text-2xs text-tertiary truncate">
|
||||
parent <a href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id}`}>
|
||||
{truncateRev(job.parent_job, 10)}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Job duration-->
|
||||
<div class="text-2xs font-normal text-secondary pr-2">
|
||||
{#if job && 'duration_ms' in job && job.duration_ms != undefined}
|
||||
{msToReadableTime(job.duration_ms, 2)}
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</div>
|
||||
{#if containsLabel}
|
||||
<div class="w-3/12 flex justify-start px-0.5">
|
||||
{#if job && job?.['labels']}
|
||||
<div class="flex flex-row items-center gap-1 overflow-x-auto">
|
||||
{#if Array.isArray(job?.['labels'])}
|
||||
{#each job?.['labels'] as label}
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs3"
|
||||
btnClasses={twMerge(
|
||||
activeLabel == label ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'!text-2xs !font-normal truncate max-w-28'
|
||||
)}
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByLabel', label)
|
||||
}}
|
||||
endIcon={{ icon: ListFilter }}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{/each}
|
||||
|
||||
<!-- Job path-->
|
||||
<div class="flex justify-start flex-col pr-4">
|
||||
{#if job === undefined}
|
||||
No job found
|
||||
{:else}
|
||||
{@const JobKindIcon = getJobKindIcon(job.job_kind)}
|
||||
<div class="flex flex-row gap-3 min-w-0 items-center h-full">
|
||||
<Tooltip class="h-full">
|
||||
<div class="relative">
|
||||
{#if job && job.parent_job}
|
||||
<span class="absolute -top-1 -right-1 text-xs text-blue-500">*</span>
|
||||
{/if}
|
||||
<JobKindIcon size={14} />
|
||||
</div>
|
||||
{#snippet text()}
|
||||
<span>
|
||||
{#if job && job.job_kind}
|
||||
{job.job_kind}
|
||||
{/if}
|
||||
{#if job && job.is_flow_step && job.parent_job}
|
||||
<br /> Step of flow
|
||||
<a href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id}`}>
|
||||
{truncateRev(job.parent_job, 10)}
|
||||
</a>
|
||||
{:else if job && job.parent_job}
|
||||
<br /> Parent
|
||||
<a href={`${base}/run/${job.parent_job}?workspace=${job.workspace_id}`}>
|
||||
{truncateRev(job.parent_job, 10)}
|
||||
</a>
|
||||
{/if}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
|
||||
<div class="whitespace-nowrap text-xs text-secondary truncate">
|
||||
{#if job.script_path}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
{#if isExternal}
|
||||
<span class="w-30 justify-center">-</span>
|
||||
{:else}
|
||||
<span class="truncate w-30" title={job.script_path}>
|
||||
{job.script_path}
|
||||
</span>
|
||||
{/if}
|
||||
{#if !isExternal || job.script_path?.startsWith('f/')}
|
||||
{@const isFolder = job.script_path?.startsWith('f/')}
|
||||
<DropdownV2
|
||||
items={() => {
|
||||
const items = isExternal
|
||||
? []
|
||||
: [
|
||||
{
|
||||
displayName: `Filter by path: ${job.script_path}`,
|
||||
action: () => dispatch('filterByPath', job.script_path),
|
||||
disabled: isExternal
|
||||
}
|
||||
]
|
||||
if (isFolder) {
|
||||
const folder = job.script_path?.split('/')[1]
|
||||
return [
|
||||
{
|
||||
displayName: `Filter by folder: ${folder}`,
|
||||
action: () => dispatch('filterByFolder', folder)
|
||||
},
|
||||
...items
|
||||
]
|
||||
}
|
||||
return items
|
||||
}}
|
||||
class="w-fit"
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<div
|
||||
class="p-1 hover:bg-surface cursor-pointer rounded-md text-gray-300 hover:text-primary"
|
||||
>
|
||||
<ListFilterPlus size={14} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if 'job_kind' in job && isScriptPreview(job.job_kind)}
|
||||
Preview without path
|
||||
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
|
||||
lock deps of {truncateHash(job.script_hash ?? '')}
|
||||
{:else if 'job_kind' in job && job.job_kind == 'identity'}
|
||||
no op
|
||||
{:else if 'job_kind' in job && isFlowPreview(job.job_kind)}
|
||||
Preview without path
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Labels-->
|
||||
{#if containsLabel}
|
||||
<div class="flex justify-start overflow-hidden" bind:clientWidth={labelWidth}>
|
||||
<RunLabels
|
||||
{job}
|
||||
{activeLabel}
|
||||
onFilterByLabel={(label) => dispatch('filterByLabel', label)}
|
||||
{labelWidth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="w-3/12 flex justify-start">
|
||||
<!-- Author and schedule-->
|
||||
<div class="flex justify-start pr-4 text-secondary">
|
||||
{#if job && job.schedule_path}
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<Calendar size={14} />
|
||||
<div class="flex flex-row items-center gap-1 w-full -ml-2">
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
btnClasses="font-normal"
|
||||
btnClasses="font-normal bg-transparent hover:bg-surface hover:text-primary"
|
||||
on:click={() => scheduleEditor?.openEdit(job.schedule_path ?? '', job.job_kind == 'flow')}
|
||||
>
|
||||
<div class="truncate text-ellipsis text-left" title={job.schedule_path}>
|
||||
{truncateRev(job.schedule_path, 20)}
|
||||
</div>
|
||||
<Calendar size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterBySchedule', job.schedule_path)
|
||||
}}
|
||||
<div class="text-xs truncate text-ellipsis text-left" dir="rtl" title={job.schedule_path}>
|
||||
{job.schedule_path}
|
||||
</div>
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: `Filter by schedule: ${truncateRev(job.schedule_path, 20)}`,
|
||||
action: () => dispatch('filterBySchedule', job.schedule_path)
|
||||
}
|
||||
]}
|
||||
class="w-fit"
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
{#snippet buttonReplacement()}
|
||||
<div
|
||||
class="p-1 hover:bg-surface cursor-pointer rounded-md text-gray-300 hover:text-primary"
|
||||
>
|
||||
<ListFilterPlus size={14} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<div class="text-xs truncate text-ellipsis text-left" title={job.created_by}>
|
||||
{truncateRev(job.created_by ?? '', 20)}
|
||||
<div class="flex flex-row gap-1 items-center w-full">
|
||||
<div class="text-xs truncate text-ellipsis text-left" dir="rtl" title={job.created_by}>
|
||||
{job.created_by ?? ''}
|
||||
</div>
|
||||
{#if !isExternal}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
dispatch('filterByUser', job.created_by ?? '')
|
||||
}}
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: `Filter by triggered by: ${job.created_by}`,
|
||||
action: () => dispatch('filterByUser', job.created_by ?? '')
|
||||
}
|
||||
]}
|
||||
customWidth={256}
|
||||
class="w-fit"
|
||||
>
|
||||
<ListFilter size={10} />
|
||||
</Button>
|
||||
{#snippet buttonReplacement()}
|
||||
<div
|
||||
class="p-1 hover:bg-surface cursor-pointer rounded-md text-gray-300 hover:text-primary"
|
||||
>
|
||||
<ListFilterPlus size={14} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Job tag-->
|
||||
{#if showTag}
|
||||
<div class="flex justify-start gap-1">
|
||||
{#if job.tag}
|
||||
<span class="text-xs text-secondary truncate" title={job.tag}>{job.tag}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Job link-->
|
||||
{#if !isExternal}
|
||||
<div class="flex justify-end pr-2">
|
||||
<a
|
||||
target="_blank"
|
||||
href="{base}/run/{job.id}?workspace={job.workspace_id}"
|
||||
class={twMerge(
|
||||
'text-right float-right px-2',
|
||||
selected
|
||||
? 'text-blue-500 hover:text-primary'
|
||||
: 'text-gray-300 dark:text-gray-500 hover:text-primary dark:hover:text-primary'
|
||||
)}
|
||||
title="See run detail in a new tab"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
export type RunsSelectionMode = 'cancel' | 're-run'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { userStore, superadmin } from '$lib/stores'
|
||||
import { X, Check, ChevronDown, Loader2 } from 'lucide-svelte'
|
||||
import { X, Check, ChevronDown, Loader2, SquareMousePointer } from 'lucide-svelte'
|
||||
import { Button } from '../common'
|
||||
import DropdownV2 from '../DropdownV2.svelte'
|
||||
|
||||
export let isLoading = false
|
||||
export let selectionCount: number
|
||||
export let selectionMode: RunsSelectionMode | false
|
||||
export let onSetSelectionMode: (mode: RunsSelectionMode | false) => void
|
||||
export let onCancelSelectedJobs: () => void
|
||||
export let onCancelFilteredJobs: () => void
|
||||
export let onReRunSelectedJobs: () => void
|
||||
export let onReRunFilteredJobs: () => void
|
||||
interface Props {
|
||||
isLoading?: boolean
|
||||
selectionCount: number
|
||||
selectionMode: RunsSelectionMode | false
|
||||
small?: boolean
|
||||
onSetSelectionMode: (mode: RunsSelectionMode | false) => void
|
||||
onCancelSelectedJobs: () => void
|
||||
onCancelFilteredJobs: () => void
|
||||
onReRunSelectedJobs: () => void
|
||||
onReRunFilteredJobs: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
isLoading = false,
|
||||
selectionCount,
|
||||
selectionMode,
|
||||
small = false,
|
||||
onSetSelectionMode,
|
||||
onCancelSelectedJobs,
|
||||
onCancelFilteredJobs,
|
||||
onReRunSelectedJobs,
|
||||
onReRunFilteredJobs
|
||||
}: Props = $props()
|
||||
|
||||
function jobCountString(count: number) {
|
||||
return `${count} ${count == 1 ? 'job' : 'jobs'}`
|
||||
@@ -27,7 +42,7 @@
|
||||
<Loader2 class="animate-spin" size={20} />
|
||||
</Button>
|
||||
{:else if selectionMode}
|
||||
<div class="mt-1 p-2 h-8 flex flex-row items-center gap-1">
|
||||
<div class="h-8 flex flex-row items-center gap-1">
|
||||
<Button
|
||||
startIcon={{ icon: X }}
|
||||
size="xs"
|
||||
@@ -62,7 +77,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<DropdownV2
|
||||
class="w-fit mx-auto"
|
||||
class="w-fit"
|
||||
items={[
|
||||
{
|
||||
displayName: 'Select jobs to cancel',
|
||||
@@ -80,13 +95,16 @@
|
||||
: [])
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
{#snippet buttonReplacement()}
|
||||
<div
|
||||
class="mt-1 p-2 h-8 flex flex-row items-center hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
class="px-2 h-[30px] border flex flex-row items-center hover:bg-surface-hover cursor-pointer rounded-md gap-2"
|
||||
>
|
||||
<span class="text-xs min-w-[5rem]">Batch actions</span>
|
||||
<ChevronDown class="w-5 h-5" />
|
||||
<SquareMousePointer size={16} />
|
||||
{#if !small}
|
||||
<span class="text-xs min-w-[5rem]">Batch actions</span>
|
||||
{/if}
|
||||
<ChevronDown size={16} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{/if}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { AlertCircle, CheckCircle2, Filter, Hourglass, PlayCircle, X } from 'lucide-svelte'
|
||||
import { CircleAlert, CircleCheck, Hourglass, ListFilterPlus, CirclePlay, X } from 'lucide-svelte'
|
||||
import JsonEditor from '../JsonEditor.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
@@ -14,6 +14,9 @@
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import Select from '../select/Select.svelte'
|
||||
import { safeSelectItems } from '../select/utils.svelte'
|
||||
import RunOption from './RunOption.svelte'
|
||||
import DropdownSelect from '../DropdownSelect.svelte'
|
||||
import TooltipV2 from '$lib/components/meltComponents/Tooltip.svelte'
|
||||
|
||||
interface Props {
|
||||
// Filters
|
||||
@@ -48,6 +51,8 @@
|
||||
| 'worker'
|
||||
| 'tag'
|
||||
| 'schedulePath'
|
||||
small?: boolean
|
||||
calendarSmall?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -72,7 +77,9 @@
|
||||
usernames = [],
|
||||
folders = [],
|
||||
allWorkspaces = $bindable(false),
|
||||
filterBy = $bindable('path')
|
||||
filterBy = $bindable('path'),
|
||||
small = false,
|
||||
calendarSmall = false
|
||||
}: Props = $props()
|
||||
|
||||
let copyArgFilter = $state(argFilter)
|
||||
@@ -115,106 +122,153 @@
|
||||
;(path || user || folder || label || worker || concurrencyKey || tag || schedulePath) &&
|
||||
untrack(() => autosetFilter())
|
||||
})
|
||||
|
||||
function resetFilter() {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
worker = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-4">
|
||||
{#if !mobile}
|
||||
<div class="flex gap-2">
|
||||
{#if $workspaceStore == 'admins'}
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Workspaces</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={allWorkspacesValue}
|
||||
on:selected={({ detail }) => (allWorkspaces = detail === 'all')}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value={'admins'} label="Admins" {item} />
|
||||
<ToggleButton value={'all'} label="All" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
{#snippet runsTooltip()}
|
||||
<TooltipV2 placement="right">
|
||||
{#snippet text()}
|
||||
'Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they
|
||||
start), they have been triggered through the UI, a schedule or webhook'
|
||||
{/snippet}
|
||||
</TooltipV2>
|
||||
{/snippet}
|
||||
{#snippet previewsTooltip()}
|
||||
<TooltipV2 placement="right">
|
||||
{#snippet text()}
|
||||
'Previews are jobs that have been started in the editor as "Tests"'
|
||||
{/snippet}
|
||||
</TooltipV2>
|
||||
{/snippet}
|
||||
{#snippet dependenciesTooltip()}
|
||||
<TooltipV2 placement="right">
|
||||
{#snippet text()}
|
||||
'Deploying a script, flow or an app launch a dependency job that creates and then attaches the
|
||||
lockfile to the deployed item. This mechanism ensures that logic is always executed with the
|
||||
exact same direct and indirect dependencies.'
|
||||
{/snippet}
|
||||
</TooltipV2>
|
||||
{/snippet}
|
||||
{#snippet syncTooltip()}
|
||||
<TooltipV2 placement="right">
|
||||
{#snippet text()}
|
||||
'Sync jobs that are triggered on every script deployment to sync the workspace with the Git
|
||||
repository configured in the workspace settings'
|
||||
{/snippet}
|
||||
</TooltipV2>
|
||||
{/snippet}
|
||||
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Filter by</span>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={filterBy}
|
||||
on:selected={(e) => {
|
||||
if (e.detail != filterBy) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
schedulePath = undefined
|
||||
{#if !mobile}
|
||||
{#if $workspaceStore == 'admins'}
|
||||
<RunOption label="Workspaces" for="workspaces">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={allWorkspacesValue}
|
||||
on:selected={({ detail }) => (allWorkspaces = detail === 'all')}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value={'admins'} label="Admins" {item} />
|
||||
<ToggleButton value={'all'} label="All" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</RunOption>
|
||||
{/if}
|
||||
<!-- Filter by -->
|
||||
<div class="flex flex-row gap-1">
|
||||
<RunOption label="Filter by" for="filter-by">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={filterBy}
|
||||
on:selected={(e) => {
|
||||
if (e.detail != filterBy) {
|
||||
resetFilter()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="path" label="Path" {item} />
|
||||
<ToggleButton value="user" label="User" {item} />
|
||||
<ToggleButton value="folder" label="Folder" {item} />
|
||||
<ToggleButtonMore
|
||||
togglableItems={[
|
||||
{ label: 'Schedule path', value: 'schedulePath' },
|
||||
{ label: 'Concurrency key', value: 'concurrencyKey' },
|
||||
{ label: 'Label', value: 'label' },
|
||||
{ label: 'Tag', value: 'tag' },
|
||||
{ label: 'Worker', value: 'worker' }
|
||||
]}
|
||||
{item}
|
||||
bind:selected={
|
||||
() => filterBy,
|
||||
(v) => {
|
||||
resetFilter()
|
||||
filterBy = v
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="path" label="Path" {item} />
|
||||
<ToggleButton value="user" label="User" {item} />
|
||||
<ToggleButton value="folder" label="Folder" {item} />
|
||||
<ToggleButtonMore
|
||||
togglableItems={[
|
||||
{ label: 'Schedule path', value: 'schedulePath' },
|
||||
{ label: 'Concurrency key', value: 'concurrencyKey' },
|
||||
{ label: 'Label', value: 'label' },
|
||||
{ label: 'Tag', value: 'tag' },
|
||||
{ label: 'Worker', value: 'worker' }
|
||||
]}
|
||||
{item}
|
||||
bind:selected={filterBy}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</RunOption>
|
||||
|
||||
{#if filterBy == 'user'}
|
||||
{#key user}
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">User</span>
|
||||
<Select
|
||||
items={safeSelectItems(usernames)}
|
||||
bind:value={() => user ?? undefined, (v) => (user = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((user = null), dispatch('reset'))}
|
||||
inputClass="!h-[32px]"
|
||||
onCreateItem={(item) => (usernames.push(item), (user = item))}
|
||||
createText="Press enter to use this value"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy == 'folder'}
|
||||
{#if filterBy == 'user'}
|
||||
{#key user}
|
||||
<RunOption label="User" for="user">
|
||||
<Select
|
||||
items={safeSelectItems(usernames)}
|
||||
bind:value={() => user ?? undefined, (v) => (user = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((user = null), dispatch('reset'))}
|
||||
inputClass="!h-[32px] min-w-36"
|
||||
onCreateItem={(item) => (usernames.push(item), (user = item))}
|
||||
createText="Press enter to use this value"
|
||||
id="user"
|
||||
/>
|
||||
</RunOption>
|
||||
{/key}
|
||||
{:else if filterBy == 'folder'}
|
||||
<RunOption label="Folder" for="folder">
|
||||
{#key folder}
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Folder</span>
|
||||
|
||||
<Select
|
||||
items={safeSelectItems(folders)}
|
||||
bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((folder = null), dispatch('reset'))}
|
||||
inputClass="!h-[32px]"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
items={safeSelectItems(folders)}
|
||||
bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((folder = null), dispatch('reset'))}
|
||||
inputClass="!h-[32px] min-w-36"
|
||||
id="folder"
|
||||
/>
|
||||
{/key}
|
||||
{:else if filterBy === 'path'}
|
||||
</RunOption>
|
||||
{:else if filterBy === 'path'}
|
||||
<RunOption label="Path" for="path">
|
||||
{#key path}
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Path</span>
|
||||
<Select
|
||||
items={safeSelectItems(paths)}
|
||||
bind:value={() => path ?? undefined, (v) => (path = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((path = null), dispatch('reset'))}
|
||||
inputClass="!h-[32px]"
|
||||
onCreateItem={(item) => (paths.push(item), (path = item))}
|
||||
createText="Press enter to use this value"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
items={safeSelectItems(paths)}
|
||||
bind:value={() => path ?? undefined, (v) => (path = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((path = null), dispatch('reset'))}
|
||||
inputClass="!h-[32px] min-w-36"
|
||||
onCreateItem={(item) => (paths.push(item), (path = item))}
|
||||
createText="Press enter to use this value"
|
||||
id="path"
|
||||
/>
|
||||
{/key}
|
||||
{:else if filterBy === 'label'}
|
||||
</RunOption>
|
||||
{:else if filterBy === 'label'}
|
||||
<RunOption label="Label" for="label">
|
||||
{#snippet tooltip()}
|
||||
<a
|
||||
href="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#jobs-labels"
|
||||
target="_blank">Job Labels</a
|
||||
> are string values in the array at the result field 'wm_labels' to easily filter them.
|
||||
{/snippet}
|
||||
{#key label}
|
||||
<div class="relative">
|
||||
{#if label}
|
||||
@@ -229,20 +283,11 @@
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span class="text-xs absolute -top-4"
|
||||
>Label <Tooltip
|
||||
><a
|
||||
href="https://www.windmill.dev/docs/core_concepts/monitor_past_and_future_runs#jobs-labels"
|
||||
target="_blank">Job Labels</a
|
||||
> are string values in the array at the result field 'wm_labels' to easily filter them.</Tooltip
|
||||
></span
|
||||
>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
class="!h-[32px] py-1 !text-xs min-w-36"
|
||||
bind:value={displayedLabel}
|
||||
onkeydown={(e) => {
|
||||
if (labelTimeout) {
|
||||
@@ -254,58 +299,59 @@
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
<div class="absolute top-10">
|
||||
<div class="absolute -top-4 right-0">
|
||||
<Toggle
|
||||
bind:checked={allowWildcards}
|
||||
size="xs"
|
||||
options={{ right: 'allow wildcards (*)' }}
|
||||
size="2xs"
|
||||
options={{ right: '(*)', title: 'allow wildcards (*)' }}
|
||||
></Toggle>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy === 'concurrencyKey'}
|
||||
</RunOption>
|
||||
{:else if filterBy === 'concurrencyKey'}
|
||||
<RunOption label="Concurrency Key" for="concurrencyKey">
|
||||
{#snippet tooltip()}
|
||||
For concurrency limited jobs, the concurrency key defines a group of jobs that share the
|
||||
same limits.
|
||||
{#if !$enterpriseLicense}
|
||||
Concurrency limits are an EE feature.
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#key concurrencyKey}
|
||||
<div class="relative">
|
||||
{#if concurrencyKey}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
onclick={() => {
|
||||
concurrencyKey = null
|
||||
dispatch('reset')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"
|
||||
>Concurrency Key <Tooltip>
|
||||
For concurrency limited jobs, the concurrency key defines a group of jobs that share
|
||||
the same limits.
|
||||
{#if !$enterpriseLicense}
|
||||
Concurrency limits are an EE feature.
|
||||
{/if}
|
||||
</Tooltip></span
|
||||
>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
bind:value={displayedConcurrencyKey}
|
||||
onkeydown={(e) => {
|
||||
if (concurrencyKeyTimeout) {
|
||||
clearTimeout(concurrencyKeyTimeout)
|
||||
}
|
||||
|
||||
concurrencyKeyTimeout = setTimeout(() => {
|
||||
concurrencyKey = displayedConcurrencyKey
|
||||
}, 1000)
|
||||
{#if concurrencyKey}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
onclick={() => {
|
||||
concurrencyKey = null
|
||||
dispatch('reset')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs min-w-36"
|
||||
bind:value={displayedConcurrencyKey}
|
||||
onkeydown={(e) => {
|
||||
if (concurrencyKeyTimeout) {
|
||||
clearTimeout(concurrencyKeyTimeout)
|
||||
}
|
||||
|
||||
concurrencyKeyTimeout = setTimeout(() => {
|
||||
concurrencyKey = displayedConcurrencyKey
|
||||
}, 1000)
|
||||
}}
|
||||
id="concurrencyKey"
|
||||
/>
|
||||
{/key}
|
||||
{:else if filterBy === 'tag'}
|
||||
</RunOption>
|
||||
{:else if filterBy === 'tag'}
|
||||
<RunOption label="Tag" for="tag">
|
||||
{#key tag}
|
||||
<div class="relative">
|
||||
{#if tag}
|
||||
@@ -319,13 +365,12 @@
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"> Tag </span>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
class="!h-[32px] py-1 !text-xs min-w-36"
|
||||
bind:value={displayedTag}
|
||||
onkeydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
@@ -336,17 +381,20 @@
|
||||
tag = displayedTag
|
||||
}, 1000)
|
||||
}}
|
||||
id="tag"
|
||||
/>
|
||||
<div class="absolute top-10">
|
||||
<div class="absolute -top-4 right-0">
|
||||
<Toggle
|
||||
bind:checked={allowWildcards}
|
||||
size="xs"
|
||||
options={{ right: 'allow wildcards (*)' }}
|
||||
size="2xs"
|
||||
options={{ right: 'wildcards (*)', title: 'allow wildcards (*)' }}
|
||||
></Toggle>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy === 'schedulePath'}
|
||||
</RunOption>
|
||||
{:else if filterBy === 'schedulePath'}
|
||||
<RunOption label="Schedule Path" for="schedulePath">
|
||||
{#key tag}
|
||||
<div class="relative">
|
||||
{#if tag}
|
||||
@@ -360,13 +408,12 @@
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"> Schedule Path </span>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
class="!h-[32px] py-1 !text-xs min-w-36"
|
||||
bind:value={displayedSchedule}
|
||||
onkeydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
@@ -377,10 +424,13 @@
|
||||
schedulePath = displayedSchedule
|
||||
}, 1000)
|
||||
}}
|
||||
id="schedulePath"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy === 'worker'}
|
||||
</RunOption>
|
||||
{:else if filterBy === 'worker'}
|
||||
<RunOption label="Worker" for="worker">
|
||||
{#key worker}
|
||||
<div class="relative">
|
||||
{#if worker}
|
||||
@@ -394,13 +444,12 @@
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"> Worker </span>
|
||||
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
class="!h-[32px] py-1 !text-xs min-w-36"
|
||||
bind:value={displayedWorker}
|
||||
onkeydown={(e) => {
|
||||
if (workerTimeout) {
|
||||
@@ -411,20 +460,70 @@
|
||||
worker = displayedWorker
|
||||
}, 1000)
|
||||
}}
|
||||
id="worker"
|
||||
/>
|
||||
<div class="absolute top-10">
|
||||
<div class="absolute -top-4 right-0">
|
||||
<Toggle
|
||||
bind:checked={allowWildcards}
|
||||
size="xs"
|
||||
options={{ right: 'allow wildcards (*)' }}
|
||||
size="2xs"
|
||||
options={{ right: 'wildcards (*)', title: 'allow wildcards (*)' }}
|
||||
></Toggle>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Kind</span>
|
||||
</RunOption>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Kind -->
|
||||
<RunOption label="Kind" for="kind">
|
||||
{#if small && !calendarSmall}
|
||||
<DropdownSelect
|
||||
btnClasses="min-w-24"
|
||||
items={[
|
||||
{
|
||||
displayName: 'All',
|
||||
action: () => {
|
||||
jobKindsCat = 'all'
|
||||
},
|
||||
id: 'all'
|
||||
},
|
||||
{
|
||||
displayName: 'Runs',
|
||||
action: () => {
|
||||
jobKindsCat = 'runs'
|
||||
},
|
||||
id: 'runs',
|
||||
extra: runsTooltip
|
||||
},
|
||||
{
|
||||
displayName: 'Previews',
|
||||
action: () => {
|
||||
jobKindsCat = 'previews'
|
||||
},
|
||||
id: 'previews',
|
||||
extra: previewsTooltip
|
||||
},
|
||||
{
|
||||
displayName: 'Deps',
|
||||
action: () => {
|
||||
jobKindsCat = 'dependencies'
|
||||
},
|
||||
id: 'dependencies',
|
||||
extra: dependenciesTooltip
|
||||
},
|
||||
{
|
||||
displayName: 'Sync',
|
||||
action: () => {
|
||||
jobKindsCat = 'deploymentcallbacks'
|
||||
},
|
||||
id: 'deploymentcallbacks',
|
||||
extra: syncTooltip
|
||||
}
|
||||
]}
|
||||
selected={jobKindsCat}
|
||||
/>
|
||||
{:else}
|
||||
<ToggleButtonGroup bind:selected={jobKindsCat}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="all" label="All" {item} />
|
||||
@@ -435,13 +534,6 @@
|
||||
tooltip="Runs are jobs that have no parent jobs (flows are jobs that are parent of the jobs they start), they have been triggered through the UI, a schedule or webhook"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="previews"
|
||||
label="Previews"
|
||||
showTooltipIcon
|
||||
tooltip="Previews are jobs that have been started in the editor as 'Tests'"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="dependencies"
|
||||
label="Deps"
|
||||
@@ -449,75 +541,94 @@
|
||||
tooltip="Deploying a script, flow or an app launch a dependency job that create and then attach the lockfile to the deployed item. This mechanism ensure that logic is always executed with the exact same direct and indirect dependencies."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="deploymentcallbacks"
|
||||
label="Sync"
|
||||
showTooltipIcon
|
||||
tooltip="Sync jobs that are triggered on every script deployment to sync the workspace with the Git repository configured in the the workspace settings"
|
||||
<ToggleButtonMore
|
||||
togglableItems={[
|
||||
{
|
||||
label: 'Previews',
|
||||
value: 'previews',
|
||||
tooltip: "Previews are jobs that have been started in the editor as 'Tests'"
|
||||
},
|
||||
{
|
||||
label: 'Sync',
|
||||
value: 'deploymentcallbacks',
|
||||
tooltip:
|
||||
'Sync jobs that are triggered on every script deployment to sync the workspace with the Git repository configured in the the workspace settings'
|
||||
}
|
||||
]}
|
||||
{item}
|
||||
bind:selected={
|
||||
() => jobKindsCat,
|
||||
(v) => {
|
||||
resetFilter()
|
||||
jobKindsCat = v
|
||||
}
|
||||
}
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Status</span>
|
||||
<ToggleButtonGroup
|
||||
selected={success ?? 'all'}
|
||||
on:selected={({ detail }) => {
|
||||
success = detail === 'all' ? undefined : detail
|
||||
dispatch('successChange', success)
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value={'all'} label="All" {item} />
|
||||
{/if}
|
||||
</RunOption>
|
||||
<!-- Status -->
|
||||
<RunOption label="Status" for="status">
|
||||
<ToggleButtonGroup
|
||||
selected={success ?? 'all'}
|
||||
on:selected={({ detail }) => {
|
||||
success = detail === 'all' ? undefined : detail
|
||||
dispatch('successChange', success)
|
||||
}}
|
||||
id="status"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value={'all'} label="All" {item} />
|
||||
<ToggleButton
|
||||
value={'running'}
|
||||
tooltip="Running"
|
||||
class="whitespace-nowrap"
|
||||
icon={CirclePlay}
|
||||
iconProps={{ color: success === 'running' ? 'blue' : 'gray' }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'success'}
|
||||
tooltip="Success"
|
||||
class="whitespace-nowrap"
|
||||
icon={CircleCheck}
|
||||
iconProps={{ color: success === 'success' ? 'green' : 'gray' }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'failure'}
|
||||
tooltip="Failure"
|
||||
class="whitespace-nowrap"
|
||||
icon={CircleAlert}
|
||||
iconProps={{ color: success === 'failure' ? 'red' : 'gray' }}
|
||||
{item}
|
||||
/>
|
||||
{#if success == 'waiting'}
|
||||
<ToggleButton
|
||||
value={'running'}
|
||||
tooltip="Running"
|
||||
value={'waiting'}
|
||||
tooltip="Waiting"
|
||||
class="whitespace-nowrap"
|
||||
icon={PlayCircle}
|
||||
iconProps={{ color: success === 'running' ? 'blue' : 'gray' }}
|
||||
icon={Hourglass}
|
||||
iconProps={{ color: 'blue' }}
|
||||
{item}
|
||||
/>
|
||||
{:else if success == 'suspended'}
|
||||
<ToggleButton
|
||||
value={'success'}
|
||||
tooltip="Success"
|
||||
value={'suspended'}
|
||||
tooltip="Suspended"
|
||||
class="whitespace-nowrap"
|
||||
icon={CheckCircle2}
|
||||
iconProps={{ color: success === 'success' ? 'green' : 'gray' }}
|
||||
icon={Hourglass}
|
||||
iconProps={{ color: 'blue' }}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value={'failure'}
|
||||
tooltip="Failure"
|
||||
class="whitespace-nowrap"
|
||||
icon={AlertCircle}
|
||||
iconProps={{ color: success === 'failure' ? 'red' : 'gray' }}
|
||||
{item}
|
||||
/>
|
||||
{#if success == 'waiting'}
|
||||
<ToggleButton
|
||||
value={'waiting'}
|
||||
tooltip="Waiting"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
iconProps={{ color: 'blue' }}
|
||||
{item}
|
||||
/>
|
||||
{:else if success == 'suspended'}
|
||||
<ToggleButton
|
||||
value={'suspended'}
|
||||
tooltip="Suspended"
|
||||
class="whitespace-nowrap"
|
||||
icon={Hourglass}
|
||||
iconProps={{ color: 'blue' }}
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</RunOption>
|
||||
{/if}
|
||||
|
||||
<RunOption label="_" for="more-filters" noLabel>
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
contentClasses="p-4"
|
||||
@@ -525,15 +636,33 @@
|
||||
usePointerDownOutside
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: Filter }}>
|
||||
More filters
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
nonCaptureEvent={true}
|
||||
startIcon={{ icon: ListFilterPlus }}
|
||||
iconOnly
|
||||
></Button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet content()}
|
||||
<Section label="Filters">
|
||||
<div class="w-102 flex flex-col gap-4">
|
||||
{#if mobile || true}
|
||||
{#if mobile}
|
||||
{#if $workspaceStore == 'admins'}
|
||||
<Label label="Workspaces">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={allWorkspacesValue}
|
||||
on:selected={({ detail }) => (allWorkspaces = detail === 'all')}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value={'admins'} label="Admins" {item} />
|
||||
<ToggleButton value={'all'} label="All" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
{/if}
|
||||
<Label label="Filter by">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={filterBy}
|
||||
@@ -739,8 +868,8 @@
|
||||
{#if filterBy === 'tag' || filterBy === 'label' || filterBy === 'worker'}
|
||||
<Toggle
|
||||
bind:checked={allowWildcards}
|
||||
size="xs"
|
||||
options={{ right: 'allow wildcards (*)' }}
|
||||
size="2xs"
|
||||
options={{ right: 'wildcards (*)', title: 'allow wildcards (*)' }}
|
||||
></Toggle>
|
||||
{/if}
|
||||
<Label label="Kind">
|
||||
@@ -816,15 +945,17 @@
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<span class="text-xs leading-6">
|
||||
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
|
||||
</span>
|
||||
<Label label="Filter by args">
|
||||
<JsonEditor bind:error={argError} bind:code={copyArgFilter} />
|
||||
</Label>
|
||||
<Label label="Filter by result">
|
||||
<JsonEditor bind:error={resultError} bind:code={copyResultFilter} />
|
||||
</Label>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs leading-6">
|
||||
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
|
||||
</span>
|
||||
<Label label="Filter by args">
|
||||
<JsonEditor bind:error={argError} bind:code={copyArgFilter} />
|
||||
</Label>
|
||||
<Label label="Filter by result">
|
||||
<JsonEditor bind:error={resultError} bind:code={copyResultFilter} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2 justify-between">
|
||||
<Button
|
||||
@@ -853,4 +984,4 @@
|
||||
</Section>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
</RunOption>
|
||||
|
||||
@@ -1,36 +1,122 @@
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import type { Tweened } from 'svelte/motion'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '../common'
|
||||
import { FilterX, ListFilter } from 'lucide-svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { Bot, Hourglass, ListFilterPlus, X } from 'lucide-svelte'
|
||||
import RunOption from './RunOption.svelte'
|
||||
import { Popover } from '../meltComponents'
|
||||
|
||||
export let queue_count: Tweened<number> | undefined = undefined
|
||||
export let suspended_count: Tweened<number> | undefined = undefined
|
||||
export let success: string | undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
interface Props {
|
||||
queue_count?: Tweened<number> | undefined
|
||||
suspended_count?: Tweened<number> | undefined
|
||||
success: string | undefined
|
||||
small?: boolean
|
||||
onJobsWaiting?: () => void
|
||||
onJobsSuspended?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
queue_count = undefined,
|
||||
suspended_count = undefined,
|
||||
success,
|
||||
small = false,
|
||||
onJobsWaiting,
|
||||
onJobsSuspended
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex gap-1 relative max-w-36 min-w-[50px] items-baseline">
|
||||
<div class="text-xs absolute -top-4 truncate flex items-baseline gap-1">Waiting for workers <Tooltip small>Jobs waiting for a worker being available to be executed</Tooltip></div>
|
||||
<div class="mt-1">{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}</div>
|
||||
<div class="truncate text-2xs !text-secondary mt-0.5">
|
||||
{#if queue_count && ($queue_count ?? 0) > 0}
|
||||
<Button size="xs2" color="light" on:click={() => dispatch('jobs_waiting')}>{#if success == 'waiting'}<FilterX size={12}></FilterX>{:else}<ListFilter size={12}></ListFilter>{/if}</Button>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
{#if small}
|
||||
<Popover contentClasses="p-4" openOnHover debounceDelay={100}>
|
||||
{#snippet trigger()}
|
||||
<div class="relative">
|
||||
<Bot size={16} />
|
||||
{#if queue_count && ($queue_count ?? 0) > 0}
|
||||
<div
|
||||
class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 bg-yellow-500 rounded-full text-white text-xs h-4 w-4"
|
||||
>
|
||||
{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
</div>
|
||||
{#snippet content()}
|
||||
{@render queuedContent()}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{:else}
|
||||
{@render queuedContent()}
|
||||
{/if}
|
||||
|
||||
{#if small}
|
||||
<Popover contentClasses="p-4" openOnHover debounceDelay={100}>
|
||||
{#snippet trigger()}
|
||||
<div class="relative">
|
||||
<Hourglass size={16} />
|
||||
<div
|
||||
class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 bg-surface-secondary-inverse rounded-full text-primary-inverse text-xs h-4 w-4"
|
||||
>
|
||||
{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
{@render suspendedContent()}
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{:else}
|
||||
{@render suspendedContent()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if suspended_count && ($suspended_count ?? 0) > 0}
|
||||
<div class="flex gap-1 relative max-w-36 min-w-[50px] items-baseline ml-20 mr-8">
|
||||
<div class="text-xs absolute -top-4 truncate flex items-baseline gap-1">Suspended <Tooltip small>Jobs waiting for an event or approval before being resumed</Tooltip></div>
|
||||
<div class="mt-1">{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}</div>
|
||||
{#snippet queuedContent()}
|
||||
<RunOption label="Waiting for workers">
|
||||
{#snippet tooltip()}
|
||||
Jobs waiting for a worker being available to be executed
|
||||
{/snippet}
|
||||
<div
|
||||
class={queue_count && ($queue_count ?? 0) > 0
|
||||
? 'bg-yellow-500 text-white rounded-full w-6 h-6 flex center-center'
|
||||
: ''}>{queue_count ? ($queue_count ?? 0).toFixed(0) : '...'}</div
|
||||
>
|
||||
<div class="truncate text-2xs !text-secondary mt-0.5">
|
||||
<Button size="xs2" color="light" on:click={() => dispatch('jobs_suspended')}>{#if success == 'waiting'}<FilterX size={12}></FilterX>{:else}<ListFilter size={12}></ListFilter>{/if}</Button>
|
||||
<Button size="xs2" color="light" on:click={() => onJobsWaiting?.()}>
|
||||
{#if success == 'waiting'}
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
Reset filter
|
||||
<X size={12} />
|
||||
</div>
|
||||
{:else}
|
||||
<ListFilterPlus size={14} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</RunOption>
|
||||
{/snippet}
|
||||
|
||||
{#snippet suspendedContent()}
|
||||
{#if suspended_count && ($suspended_count ?? 0) > 0}
|
||||
<RunOption label="Suspended">
|
||||
{#snippet tooltip()}
|
||||
Jobs waiting for an event or approval before being resumed
|
||||
{/snippet}
|
||||
<div
|
||||
class={suspended_count && ($suspended_count ?? 0) > 0
|
||||
? 'bg-surface-secondary-inverse text-primary-inverse rounded-full w-6 h-6 flex center-center'
|
||||
: ''}>{suspended_count ? ($suspended_count ?? 0).toFixed(0) : '...'}</div
|
||||
>
|
||||
<div class="truncate text-2xs !text-secondary">
|
||||
<Button size="xs2" color="light" on:click={() => onJobsSuspended?.()}>
|
||||
{#if success == 'suspended'}
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
Reset filter
|
||||
<X size={12} />
|
||||
</div>
|
||||
{:else}
|
||||
<ListFilterPlus size={14} />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</RunOption>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { createBubbler } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
import type { Job } from '$lib/gen'
|
||||
import RunRow from './RunRow.svelte'
|
||||
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
|
||||
@@ -10,9 +7,8 @@
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { isJobSelectable } from '$lib/utils'
|
||||
import type { RunsSelectionMode } from './RunsBatchActionsDropdown.svelte'
|
||||
import './runs-grid.css'
|
||||
|
||||
interface Props {
|
||||
//import InfiniteLoading from 'svelte-infinite-loading'
|
||||
@@ -138,25 +134,6 @@
|
||||
}
|
||||
*/
|
||||
|
||||
let selectableJobCount = $derived.by(() => {
|
||||
if (!selectionMode) return 0
|
||||
return jobs?.filter(isJobSelectable(selectionMode)).length ?? 0
|
||||
})
|
||||
let allSelected = $derived.by(() => {
|
||||
return selectionMode && selectedIds.length === selectableJobCount
|
||||
})
|
||||
|
||||
function selectAll() {
|
||||
if (!selectionMode) return
|
||||
if (allSelected) {
|
||||
allSelected = false
|
||||
selectedIds = []
|
||||
} else {
|
||||
allSelected = true
|
||||
selectedIds = jobs?.filter(isJobSelectable(selectionMode)).map((j) => j.id) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
function jobCountString(jobCount: number | undefined, lastFetchWentToEnd: boolean): string {
|
||||
if (jobCount === undefined) {
|
||||
return ''
|
||||
@@ -213,64 +190,67 @@
|
||||
}
|
||||
return nstickyIndices
|
||||
})
|
||||
|
||||
const showTag = $derived(containerWidth > 700)
|
||||
</script>
|
||||
|
||||
<svelte:window onresize={() => computeHeight()} />
|
||||
|
||||
<div
|
||||
class="divide-y min-w-[640px] h-full"
|
||||
class="divide-y h-full border min-w-[650px]"
|
||||
id="runs-table-wrapper"
|
||||
bind:clientWidth={containerWidth}
|
||||
>
|
||||
<div bind:clientHeight={headerHeight}>
|
||||
{#if selectionMode && selectableJobCount}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover bg-surface-primary cursor-pointer',
|
||||
allSelected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'flex flex-row items-center sticky w-full p-2 pr-4 top-0 font-semibold border-t text-sm'
|
||||
)}
|
||||
onclick={selectAll}
|
||||
>
|
||||
<div class="px-2">
|
||||
<input onfocus={bubble('focus')} type="checkbox" checked={allSelected} />
|
||||
</div>
|
||||
Select all
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-row bg-surface-secondary sticky top-0 w-full p-2 pr-4">
|
||||
{#if showExternalJobs && externalJobs.length > 0}
|
||||
<div class="w-1/12 text-2xs">
|
||||
<div
|
||||
class="grid bg-surface-secondary sticky top-0 w-full py-2 pr-4"
|
||||
class:grid-runs-table={!containsLabel && !selectionMode && showTag}
|
||||
class:grid-runs-table-with-labels={containsLabel && !selectionMode && showTag}
|
||||
class:grid-runs-table-selection={!containsLabel && selectionMode && showTag}
|
||||
class:grid-runs-table-with-labels-selection={containsLabel && selectionMode && showTag}
|
||||
class:grid-runs-table-no-tag={!containsLabel && !selectionMode && !showTag}
|
||||
class:grid-runs-table-with-labels-no-tag={containsLabel && !selectionMode && !showTag}
|
||||
class:grid-runs-table-selection-no-tag={!containsLabel && selectionMode && !showTag}
|
||||
class:grid-runs-table-with-labels-selection-no-tag={containsLabel &&
|
||||
selectionMode &&
|
||||
!showTag}
|
||||
>
|
||||
{#if selectionMode}
|
||||
<div class="text-xs font-semibold pl-4"></div>
|
||||
{/if}
|
||||
<div class="text-2xs px-2 flex flex-row items-center gap-2">
|
||||
{#if showExternalJobs && externalJobs.length > 0}
|
||||
<div class="flex flex-row">
|
||||
{jobs
|
||||
? jobCountString(jobs.length + externalJobs.length, lastFetchWentToEnd)
|
||||
: ''}<Tooltip>{externalJobs.length} jobs obscured</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $workspaceStore !== 'admins' && omittedObscuredJobs}
|
||||
<div class="w-1/12 text-2xs flex flex-row">
|
||||
{:else if $workspaceStore !== 'admins' && omittedObscuredJobs}
|
||||
<div class="flex flex-row">
|
||||
{jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''}
|
||||
<Popover>
|
||||
<AlertTriangle size={16} class="ml-0.5 text-yellow-500" />
|
||||
{#snippet text()}
|
||||
Too specific filtering may have caused the omission of obscured jobs. This is done
|
||||
for security reasons. To see obscured jobs, try removing some filters.
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{:else}
|
||||
{jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''}
|
||||
<Popover>
|
||||
<AlertTriangle size={16} class="ml-0.5 text-yellow-500" />
|
||||
{#snippet text()}
|
||||
Too specific filtering may have caused the omission of obscured jobs. This is done for
|
||||
security reasons. To see obscured jobs, try removing some filters.
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-1/12 text-2xs"
|
||||
>{jobs ? jobCountString(jobs.length, lastFetchWentToEnd) : ''}</div
|
||||
>
|
||||
{/if}
|
||||
<div class="w-4/12 text-xs font-semibold"></div>
|
||||
<div class="w-4/12 text-xs font-semibold">Path</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-xs font-semibold">Started</div>
|
||||
<div class="text-xs font-semibold">Duration</div>
|
||||
<div class="text-xs font-semibold">Path</div>
|
||||
{#if containsLabel}
|
||||
<div class="w-3/12 text-xs font-semibold">Label</div>
|
||||
<div class="text-xs font-semibold">Label</div>
|
||||
{/if}
|
||||
<div class="w-3/12 text-xs font-semibold">Triggered by</div>
|
||||
<div class="text-xs font-semibold">Triggered by</div>
|
||||
{#if showTag}
|
||||
<div class="text-xs font-semibold">Tag</div>
|
||||
{/if}
|
||||
<div class=""></div>
|
||||
</div>
|
||||
</div>
|
||||
{#if jobs?.length == 0 && (!showExternalJobs || externalJobs?.length == 0)}
|
||||
@@ -294,13 +274,16 @@
|
||||
|
||||
{#if jobOrDate}
|
||||
{#if jobOrDate?.type === 'date'}
|
||||
<div class="bg-surface-secondary py-2 border-b font-semibold text-xs pl-5">
|
||||
<div
|
||||
class="bg-surface-secondary py-2 border-b font-semibold text-xs pl-2 h-[42px] flex items-center"
|
||||
>
|
||||
{jobOrDate.date}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center h-full w-full">
|
||||
<RunRow
|
||||
{containsLabel}
|
||||
{showTag}
|
||||
job={jobOrDate.job}
|
||||
selected={jobOrDate.job.id !== '-' && selectedIds.includes(jobOrDate.job.id)}
|
||||
{selectionMode}
|
||||
@@ -314,9 +297,18 @@
|
||||
selectedIds = selectedIds
|
||||
}
|
||||
} else {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
if (
|
||||
JSON.stringify(selectedIds) !== JSON.stringify([jobOrDate.job.id]) ||
|
||||
selectedWorkspace !== jobOrDate.job.workspace_id
|
||||
) {
|
||||
selectedWorkspace = jobOrDate.job.workspace_id
|
||||
selectedIds = [jobOrDate.job.id]
|
||||
dispatch('select')
|
||||
} else {
|
||||
selectedIds = []
|
||||
selectedWorkspace = undefined
|
||||
dispatch('select')
|
||||
}
|
||||
}
|
||||
}}
|
||||
{activeLabel}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/* Shared CSS Grid Styles for RunsTable and RunRow components */
|
||||
|
||||
/* Grid layouts with tag column */
|
||||
.grid-runs-table {
|
||||
grid-template-columns:
|
||||
60px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(200px, 2.3fr) /* Path (~33% reduced for wider tag) */
|
||||
minmax(150px, 1.8fr) /* Triggered by (~28% reduced for wider tag) */
|
||||
minmax(70px, 0.5fr) /* Tag (~7% wider) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
.grid-runs-table-with-labels {
|
||||
grid-template-columns:
|
||||
60px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(150px, 1.6fr) /* Path (~25% reduced for labels and wider tag) */
|
||||
minmax(120px, 1.3fr) /* Labels (~22% reduced for wider tag) */
|
||||
minmax(110px, 1.3fr) /* Triggered by (~22% reduced for wider tag) */
|
||||
minmax(70px, 0.5fr) /* Tag (~7% wider) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
.grid-runs-table-selection {
|
||||
grid-template-columns:
|
||||
50px /* Selection checkbox (fixed width) */
|
||||
80px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(200px, 2.3fr) /* Path (~33% reduced for wider tag) */
|
||||
minmax(150px, 1.8fr) /* Triggered by (~28% reduced for wider tag) */
|
||||
minmax(70px, 0.5fr) /* Tag (~7% wider) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
.grid-runs-table-with-labels-selection {
|
||||
grid-template-columns:
|
||||
50px /* Selection checkbox (fixed width) */
|
||||
80px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(150px, 1.6fr) /* Path (~25% reduced for labels and wider tag) */
|
||||
minmax(120px, 1.3fr) /* Labels (~22% reduced for wider tag) */
|
||||
minmax(110px, 1.3fr) /* Triggered by (~22% reduced for wider tag) */
|
||||
minmax(70px, 0.5fr) /* Tag (~7% wider) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
/* Grid layouts without tag column */
|
||||
.grid-runs-table-no-tag {
|
||||
grid-template-columns:
|
||||
60px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(200px, 2.8fr) /* Path (~38% expanded without tag) */
|
||||
minmax(150px, 2.2fr) /* Triggered by (~33% expanded without tag) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
.grid-runs-table-with-labels-no-tag {
|
||||
grid-template-columns:
|
||||
60px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(150px, 1.8fr) /* Path (~30% expanded without tag) */
|
||||
minmax(120px, 1.8fr) /* Labels (~30% expanded without tag) */
|
||||
minmax(110px, 1.8fr) /* Triggered by (~30% expanded without tag) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
.grid-runs-table-selection-no-tag {
|
||||
grid-template-columns:
|
||||
40px /* Selection checkbox (fixed width) */
|
||||
60px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(200px, 2.8fr) /* Path (~38% expanded without tag) */
|
||||
minmax(150px, 2.2fr) /* Triggered by (~33% expanded without tag) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
|
||||
.grid-runs-table-with-labels-selection-no-tag {
|
||||
grid-template-columns:
|
||||
50px /* Selection checkbox (fixed width) */
|
||||
60px /* Status (fixed width) */
|
||||
minmax(100px, 0.8fr) /* Started time (~10%) */
|
||||
minmax(60px, 0.5fr) /* Duration (~7%) */
|
||||
minmax(150px, 1.8fr) /* Path (~30% expanded without tag) */
|
||||
minmax(120px, 1.8fr) /* Labels (~30% expanded without tag) */
|
||||
minmax(110px, 1.8fr) /* Triggered by (~30% expanded without tag) */
|
||||
minmax(40px, 0.3fr); /* Actions (~5%) */
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type Schema, modalToSchema, type ModalSchemaProperty } from '$lib/common'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import AddPropertyFormV2 from './AddPropertyFormV2.svelte'
|
||||
interface Props {
|
||||
schema?: Schema | any
|
||||
trigger?: import('svelte').Snippet
|
||||
noPopover?: boolean
|
||||
onAddNew?: (argName: string) => void
|
||||
}
|
||||
|
||||
let { schema = $bindable(emptySchema()), trigger, noPopover }: Props = $props()
|
||||
let { schema = $bindable(emptySchema()), trigger, noPopover, onAddNew }: Props = $props()
|
||||
|
||||
export const DEFAULT_PROPERTY: ModalSchemaProperty = {
|
||||
selectedType: 'string',
|
||||
@@ -18,8 +18,6 @@
|
||||
required: false
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
if (!schema) {
|
||||
schema = emptySchema()
|
||||
}
|
||||
@@ -107,7 +105,6 @@
|
||||
editing = false
|
||||
oldArgName = undefined
|
||||
schema = $state.snapshot(newSchema)
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
if (argError !== '') {
|
||||
@@ -139,14 +136,11 @@
|
||||
if (modifiedObject.order) {
|
||||
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
|
||||
}
|
||||
dispatch('change', schema)
|
||||
} else {
|
||||
throw Error('Argument not found!')
|
||||
}
|
||||
syncOrders(modifiedObject)
|
||||
schema = $state.snapshot(modifiedObject)
|
||||
|
||||
dispatch('change', schema)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete argument: ${err}`, true)
|
||||
}
|
||||
@@ -164,7 +158,7 @@
|
||||
selectedType: 'string',
|
||||
name: e.detail.name
|
||||
})
|
||||
dispatch('addNew', e.detail.name)
|
||||
onAddNew?.(e.detail.name)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not add argument: ${err}`, true)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import AddProperty from './AddProperty.svelte'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { emptyString, generateRandomString } from '$lib/utils'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import Label from '../Label.svelte'
|
||||
@@ -50,11 +50,16 @@
|
||||
|
||||
interface Props {
|
||||
schema: Schema | undefined | any
|
||||
parentId?: string | undefined
|
||||
jsonView?: boolean
|
||||
hiddenArgs?: string[]
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
let { schema = $bindable(), parentId = undefined, jsonView = $bindable(false) }: Props = $props()
|
||||
let {
|
||||
schema = $bindable(),
|
||||
jsonView = $bindable(false),
|
||||
hiddenArgs = undefined
|
||||
}: Props = $props()
|
||||
|
||||
// let schema = $state(structuredClone($state.snapshot(schema)))
|
||||
|
||||
@@ -63,17 +68,32 @@
|
||||
let error: string | undefined = $state(undefined)
|
||||
let items = $derived([
|
||||
...new Set(
|
||||
(schema?.order ?? Object.keys(schema?.properties ?? {}))?.map((item, index) => {
|
||||
return { value: item, id: item }
|
||||
}) ?? []
|
||||
(schema?.order ?? Object.keys(schema?.properties ?? {}))
|
||||
?.map((item, index) => {
|
||||
return { value: item, id: item }
|
||||
})
|
||||
.filter((item) => !hiddenArgs?.includes(item.value)) ?? []
|
||||
)
|
||||
]) as Array<{
|
||||
value: string
|
||||
id: string
|
||||
}>
|
||||
|
||||
const rnd = generateRandomString()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-end mb-2 w-full">
|
||||
<div class="flex flex-wrap justify-between mb-2 w-full items-center gap-y-2">
|
||||
<AddProperty
|
||||
on:change={() => {
|
||||
if (jsonView) {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}
|
||||
}}
|
||||
bind:schema
|
||||
bind:this={addPropertyComponent}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
@@ -91,136 +111,116 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AddProperty
|
||||
on:change={() => {
|
||||
if (jsonView) {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}
|
||||
}}
|
||||
bind:schema
|
||||
bind:this={addPropertyComponent}
|
||||
/>
|
||||
|
||||
{#if !jsonView}
|
||||
<div
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: parentId ? `app-editor-fields-${parentId}` : 'app-editor-fields'
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
class="gap-1 flex flex-col mt-2"
|
||||
>
|
||||
{#if items?.length > 0}
|
||||
{#each items as item (item.id)}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
class="w-full flex flex-col justify-between border items-center py-1 px-2 rounded-md bg-surface text-sm"
|
||||
>
|
||||
{#if schema.properties?.[item.value]}
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
{`${item.value}${
|
||||
schema.properties?.[item.value]?.title
|
||||
? ` (title: ${schema.properties?.[item.value]?.title})`
|
||||
: ''
|
||||
} `}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="flex flex-row gap-1 item-center h-full justify-center">
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Trash }}
|
||||
on:click={() => {
|
||||
addPropertyComponent?.handleDeleteArgument([item.value])
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Pen }}
|
||||
on:click={() => {
|
||||
schemaFormDrawer?.openDrawer()
|
||||
{#key rnd}
|
||||
<div
|
||||
use:dragHandleZone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: rnd
|
||||
}}
|
||||
onconsider={handleConsider}
|
||||
onfinalize={handleFinalize}
|
||||
class="gap-1 flex flex-col mt-2"
|
||||
>
|
||||
{#if items?.length > 0}
|
||||
{#each items as item (item.id)}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div
|
||||
animate:flip={{ duration: 200 }}
|
||||
class="w-full flex flex-col justify-between border items-center py-1 px-2 rounded-md bg-surface text-sm"
|
||||
>
|
||||
{#if schema.properties?.[item.value]}
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
{`${item.value}${
|
||||
schema.properties?.[item.value]?.title
|
||||
? ` (title: ${schema.properties?.[item.value]?.title})`
|
||||
: ''
|
||||
} `}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="flex flex-row gap-1 item-center h-full justify-center">
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Trash }}
|
||||
on:click={() => {
|
||||
addPropertyComponent?.handleDeleteArgument([item.value])
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
iconOnly
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Pen }}
|
||||
on:click={() => {
|
||||
schemaFormDrawer?.openDrawer()
|
||||
|
||||
tick().then(() => {
|
||||
editableSchemaForm?.openField(item.value)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
tick().then(() => {
|
||||
editableSchemaForm?.openField(item.value)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="cursor-move flex items-center handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
<div class="flex items-center handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if schema.properties[item.value]?.type === 'object' && !(schema.properties[item.value].oneOf && schema.properties[item.value].oneOf.length >= 2)}
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<Label label="Nested properties">
|
||||
<EditableSchemaDrawer
|
||||
on:change={() => {
|
||||
schema = $state.snapshot(schema)
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
bind:schema={schema.properties[item.value]}
|
||||
parentId={item.value}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
{#if schema.properties[item.value]?.type === 'object' && !(schema.properties[item.value].oneOf && schema.properties[item.value].oneOf.length >= 2)}
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<Label label="Nested properties">
|
||||
<EditableSchemaDrawer bind:schema={schema.properties[item.value]} />
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-tertiary"> Value is undefined </div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-tertiary"> Value is undefined </div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
<Drawer bind:this={schemaFormDrawer} size="1200px">
|
||||
<DrawerContent title="UI Customisation" on:close={schemaFormDrawer.closeDrawer}>
|
||||
<EditableSchemaForm
|
||||
on:change={(e) => {
|
||||
schema = $state.snapshot(schema)
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
isAppInput
|
||||
on:edit={(e) => {
|
||||
addPropertyComponent?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addPropertyComponent?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
dndType="drawer"
|
||||
editTab="inputEditor"
|
||||
>
|
||||
{#snippet addProperty()}
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
on:change
|
||||
on:addNew={(e) => {
|
||||
schema = $state.snapshot(schema)
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
}}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</AddPropertyV2>
|
||||
{/snippet}
|
||||
</EditableSchemaForm>
|
||||
</DrawerContent>
|
||||
{#snippet children()}
|
||||
<DrawerContent title="UI Customisation" on:close={() => schemaFormDrawer?.closeDrawer()}>
|
||||
<EditableSchemaForm
|
||||
schemaFormClassName="h-full"
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
isAppInput
|
||||
on:edit={(e) => {
|
||||
addPropertyComponent?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addPropertyComponent?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
{hiddenArgs}
|
||||
editTab="inputEditor"
|
||||
>
|
||||
{#snippet addProperty()}
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
onAddNew={(argName) => {
|
||||
editableSchemaForm?.openField(argName)
|
||||
}}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</AddPropertyV2>
|
||||
{/snippet}
|
||||
</EditableSchemaForm>
|
||||
</DrawerContent>
|
||||
{/snippet}
|
||||
</Drawer>
|
||||
{:else}
|
||||
<div class="mt-2"></div>
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { readFieldsRecursively } from '$lib/utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { EditableSchemaWrapperProps } from './editable_schema_wrapper'
|
||||
import EditableSchemaWrapper from './EditableSchemaWrapper.svelte'
|
||||
|
||||
let { schema: oldSchema, ...props }: EditableSchemaWrapperProps = $props()
|
||||
let {
|
||||
schema: oldSchema,
|
||||
onSchemaChange,
|
||||
...props
|
||||
}: EditableSchemaWrapperProps & { onSchemaChange?: (schema: any) => void } = $props()
|
||||
|
||||
let schema = $state(oldSchema)
|
||||
|
||||
let lastSchema = $state.snapshot(schema)
|
||||
$effect(() => {
|
||||
if (onSchemaChange) {
|
||||
readFieldsRecursively(schema)
|
||||
let newSchema = $state.snapshot(schema)
|
||||
if (!deepEqual(lastSchema, newSchema)) {
|
||||
lastSchema = newSchema
|
||||
onSchemaChange(newSchema)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<EditableSchemaWrapper {schema} {...props} />
|
||||
<EditableSchemaWrapper bind:schema {...props} />
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
noPreview = false,
|
||||
fullHeight = true,
|
||||
formatExtension = $bindable(undefined),
|
||||
onSchemaChange,
|
||||
customUi
|
||||
}: EditableSchemaWrapperProps = $props()
|
||||
|
||||
@@ -51,7 +50,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
onSchemaChange?.({ schema: $state.snapshot(schema) })
|
||||
}
|
||||
|
||||
let suggestedFileExtensions = $state([
|
||||
@@ -81,9 +79,8 @@
|
||||
noPopover={customUi?.noAddPopover}
|
||||
bind:schema
|
||||
bind:this={addPropertyComponent}
|
||||
on:change={() => onSchemaChange?.({ schema: $state.snapshot(schema) })}
|
||||
on:addNew={(e) => {
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
onAddNew={(argName) => {
|
||||
editableSchemaForm?.openField(argName)
|
||||
}}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
@@ -99,11 +96,7 @@
|
||||
onlyMaskPassword
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
on:change={() => onSchemaChange?.({ schema: $state.snapshot(schema) })}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addPropertyComponent?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addPropertyComponent?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
@@ -117,7 +110,6 @@
|
||||
noPopover={customUi?.noAddPopover}
|
||||
bind:schema
|
||||
bind:this={addPropertyComponent}
|
||||
on:change={() => onSchemaChange?.({ schema })}
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<div
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
import type VariableEditor from '../VariableEditor.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ArgInput from '../ArgInput.svelte'
|
||||
import ObjectTypeNarrowing from '../ObjectTypeNarrowing.svelte'
|
||||
import Tabs from '../common/tabs/Tabs.svelte'
|
||||
import { Tab, TabContent } from '../common'
|
||||
import ResourceNarrowing from '../ResourceNarrowing.svelte'
|
||||
import EditableSchemaDrawer from './EditableSchemaDrawer.svelte'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
@@ -18,7 +16,6 @@
|
||||
import { Pen, Plus, Trash2 } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import ResourcePicker from '../ResourcePicker.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
|
||||
interface Props {
|
||||
format?: string | undefined
|
||||
@@ -49,6 +46,7 @@
|
||||
order?: string[] | undefined
|
||||
requiredProperty?: string[] | undefined
|
||||
displayWebhookWarning?: boolean
|
||||
onDrawerClose?: () => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -72,10 +70,11 @@
|
||||
properties = $bindable(undefined),
|
||||
order = $bindable(undefined),
|
||||
requiredProperty = $bindable(undefined),
|
||||
displayWebhookWarning = true
|
||||
displayWebhookWarning = true,
|
||||
onDrawerClose = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let oneOfSelected: string | undefined = $state(undefined)
|
||||
let oneOfSelected: string | undefined = $state(oneOf?.[0]?.title)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -139,254 +138,215 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 mt-2">
|
||||
{#if type === 'object' && oneOf && oneOf.length >= 2}
|
||||
<div class="flex flex-row gap-1 items-center justify-start">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={oneOfSelected}
|
||||
class="h-auto w-auto"
|
||||
tabListClass="flex-wrap"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
{#each oneOf ?? [] as obj}
|
||||
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
<Label label="OneOf properties">
|
||||
<div class="flex flex-row gap-1 items-center justify-start pt-2">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={oneOfSelected}
|
||||
class="h-auto w-auto"
|
||||
tabListClass="flex-wrap"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
{#each oneOf ?? [] as obj}
|
||||
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<Popover placement="bottom-end" closeButton>
|
||||
{#snippet trigger()}
|
||||
<Button size="xs2" color="light" nonCaptureEvent startIcon={{ icon: Plus }} />
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<Label label="Label" class="p-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
<Popover placement="bottom-end" closeButton>
|
||||
{#snippet trigger()}
|
||||
<Button size="xs2" color="light" nonCaptureEvent startIcon={{ icon: Plus }} />
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<Label label="Label" class="p-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
createVariant(variantName)
|
||||
close()
|
||||
}
|
||||
}}
|
||||
bind:value={variantName}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
createVariant(variantName)
|
||||
close()
|
||||
}}
|
||||
disabled={variantName.length === 0}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Label>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center ml-1 mt-4 mb-2">
|
||||
<span class="font-semibold text-sm">{oneOfSelected}</span>
|
||||
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
closeButton
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Pen }}
|
||||
propagateEvent
|
||||
iconOnly={false}
|
||||
on:click={() => {
|
||||
if (oneOfSelected) {
|
||||
variantName = oneOfSelected
|
||||
}
|
||||
}}
|
||||
bind:value={variantName}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
createVariant(variantName)
|
||||
close()
|
||||
}}
|
||||
disabled={variantName.length === 0}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Label>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<span class="font-semibold text-sm">{oneOfSelected}</span>
|
||||
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
closeButton
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Pen }}
|
||||
propagateEvent
|
||||
iconOnly={false}
|
||||
on:click={() => {
|
||||
if (oneOfSelected) {
|
||||
variantName = oneOfSelected
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<Label label="Label" class="p-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
{/snippet}
|
||||
{#snippet content({ close })}
|
||||
<Label label="Label" class="p-2 flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (oneOfSelected) {
|
||||
renameVariant(variantName, oneOfSelected)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}}
|
||||
bind:value={variantName}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (oneOfSelected) {
|
||||
renameVariant(variantName, oneOfSelected)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}}
|
||||
bind:value={variantName}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (oneOfSelected) {
|
||||
renameVariant(variantName, oneOfSelected)
|
||||
close()
|
||||
}
|
||||
}}
|
||||
disabled={variantName.length === 0}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</Label>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
iconOnly
|
||||
disabled={(oneOf?.length ?? 0) <= 2}
|
||||
on:click={() => {
|
||||
if (oneOf && oneOfSelected) {
|
||||
const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)
|
||||
oneOf = oneOf.filter((_, i) => i !== idx)
|
||||
oneOfSelected = oneOf[0].title
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if oneOfSelected && oneOf}
|
||||
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
||||
<EditableSchemaDrawer
|
||||
bind:schema={
|
||||
() => {
|
||||
if (oneOf?.[idx]) {
|
||||
let properties = Object.fromEntries(
|
||||
Object.entries(oneOf[idx].properties ?? {}).filter(
|
||||
([k]) => k !== 'label' && k !== 'kind'
|
||||
)
|
||||
)
|
||||
return {
|
||||
...oneOf[idx],
|
||||
properties: properties,
|
||||
order: Object.keys(properties),
|
||||
required: oneOf[idx].required ?? []
|
||||
}
|
||||
}}
|
||||
disabled={variantName.length === 0}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</Label>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
iconOnly
|
||||
disabled={(oneOf?.length ?? 0) <= 2}
|
||||
on:click={() => {
|
||||
if (oneOf && oneOfSelected) {
|
||||
const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)
|
||||
oneOf = oneOf.filter((_, i) => i !== idx)
|
||||
oneOfSelected = oneOf[0].title
|
||||
}
|
||||
},
|
||||
(v) => {
|
||||
if (oneOf?.[idx]) {
|
||||
const tagKey = oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if oneOfSelected && oneOf}
|
||||
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
||||
<div class="ml-1">
|
||||
<EditableSchemaDrawer
|
||||
onClose={() => {
|
||||
onDrawerClose?.()
|
||||
}}
|
||||
bind:schema={oneOf[idx]}
|
||||
hiddenArgs={[
|
||||
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
|
||||
? 'kind'
|
||||
: 'label'
|
||||
oneOf[idx] = {
|
||||
...(v ?? {}),
|
||||
type: 'object',
|
||||
properties: {
|
||||
...(v?.properties ?? {}),
|
||||
[tagKey]: {
|
||||
type: 'string',
|
||||
enum: [v?.title ?? '']
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
<div class="py-2"></div>
|
||||
{:else if type === 'object' && format?.startsWith('resource-') && format !== 'resource-s3_object' && !isDynSelect}
|
||||
<ResourceNarrowing bind:format />
|
||||
{:else if type === 'object' && !format?.startsWith('resource-') && !isDynSelect}
|
||||
<div class="py-2">
|
||||
<Label label="Object properties">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={customObjectSelected}
|
||||
class="my-2"
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'editor') {
|
||||
format = undefined
|
||||
} else {
|
||||
properties = undefined
|
||||
order = undefined
|
||||
requiredProperty = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="editor" label="Custom" {item} />
|
||||
<ToggleButton
|
||||
value="json-schema-resource"
|
||||
label="Template"
|
||||
{item}
|
||||
tooltip="Select a JSON schema resource to specify the object's properties"
|
||||
showTooltipIcon
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if customObjectSelected === 'editor'}
|
||||
<EditableSchemaDrawer
|
||||
bind:schema={
|
||||
() => {
|
||||
return {
|
||||
properties: properties,
|
||||
order: order,
|
||||
required: requiredProperty
|
||||
}
|
||||
},
|
||||
(v) => {
|
||||
properties = v.properties
|
||||
order = v.order
|
||||
requiredProperty = v.required
|
||||
}
|
||||
}
|
||||
/>
|
||||
{:else if customObjectSelected === 'json-schema-resource'}
|
||||
{#if format == undefined}
|
||||
<div class="text-xs text-tertiary my-1">
|
||||
Select a <code>json_schema</code> resource as a reusable JSON schema template
|
||||
</div>
|
||||
{/if}
|
||||
<ResourcePicker
|
||||
resourceType="json_schema"
|
||||
bind:value={
|
||||
() => {
|
||||
if (format?.startsWith('jsonschema-')) {
|
||||
return format.substring('jsonschema-'.length)
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
(v) => {
|
||||
if (v) {
|
||||
format = 'jsonschema-' + v
|
||||
} else {
|
||||
format = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
on:change={() => {
|
||||
dispatch('schemaChange')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if type === 'object' && format !== 'resource-s3_object' && !isDynSelect}
|
||||
<Tabs
|
||||
bind:selected={initialObjectSelected}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'json-schema') {
|
||||
format = 'json-schema'
|
||||
} else {
|
||||
format = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab value="resource">Resource</Tab>
|
||||
<Tab value="custom-object">Custom Object</Tab>
|
||||
<Tab value="json-schema">
|
||||
JSON Schema
|
||||
<Tooltip>
|
||||
This displays a JSON schema editor, useful when a JSON schema input is expected.
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
{#snippet content()}
|
||||
<div class="pt-2">
|
||||
<TabContent value="custom-object">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={customObjectSelected}
|
||||
class="mb-2"
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'editor') {
|
||||
format = undefined
|
||||
} else {
|
||||
properties = undefined
|
||||
order = undefined
|
||||
requiredProperty = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="editor" label="Editor" {item} />
|
||||
<ToggleButton
|
||||
value="json-schema-resource"
|
||||
label="JSON Schema Resource"
|
||||
{item}
|
||||
tooltip="Select a JSON schema resource to specify the object's properties"
|
||||
showTooltipIcon
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if customObjectSelected === 'editor'}
|
||||
<EditableSchemaDrawer
|
||||
bind:schema={
|
||||
() => {
|
||||
return {
|
||||
properties: properties,
|
||||
order: order,
|
||||
required: requiredProperty
|
||||
}
|
||||
},
|
||||
(v) => {
|
||||
properties = v.properties
|
||||
order = v.order
|
||||
requiredProperty = v.required
|
||||
dispatch('schemaChange')
|
||||
}
|
||||
}
|
||||
/>
|
||||
{:else if customObjectSelected === 'json-schema-resource'}
|
||||
<ResourcePicker
|
||||
resourceType="json_schema"
|
||||
bind:value={
|
||||
() => {
|
||||
if (format?.startsWith('jsonschema-')) {
|
||||
return format.substring('jsonschema-'.length)
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
(v) => {
|
||||
format = 'jsonschema-' + v
|
||||
}
|
||||
}
|
||||
/>
|
||||
{/if}
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="resource">
|
||||
<ObjectTypeNarrowing on:change={() => dispatch('schemaChange')} bind:format />
|
||||
</TabContent>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
/>
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !(type === 'object' && oneOf && oneOf.length >= 2) && !(type == 'object' && initialObjectSelected == 'custom-object') && !isDynSelect}
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
bind:itemsType
|
||||
canEditResourceType={isFlowInput || isAppInput}
|
||||
/>
|
||||
{:else if type == 'string' || ['number', 'integer', 'object'].includes(type ?? '')}
|
||||
{:else if type == 'string' || ['number', 'integer'].includes(type ?? '')}
|
||||
<div>
|
||||
<Label label="Field settings">
|
||||
<div>
|
||||
@@ -244,44 +244,42 @@
|
||||
bind:currency={extra['currency']}
|
||||
bind:currencyLocale={extra['currencyLocale']}
|
||||
/>
|
||||
{:else if type == 'object' && oneOf && oneOf.length >= 2 && !isFlowInput && !isAppInput}
|
||||
<ToggleButtonGroup bind:selected={oneOfSelected} class="mb-2">
|
||||
{#snippet children({ item })}
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if oneOfSelected && oneOfSchemas}
|
||||
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
||||
<div class="border">
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
noPreview
|
||||
bind:schema={oneOfSchemas[idx]}
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
editTab="inputEditor"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
||||
<div class="border">
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
noPreview
|
||||
bind:schema
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
editTab="inputEditor"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-tertiary text-xs">No settings available for this field type</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{:else if type == 'object' && oneOf && oneOf.length >= 2 && !isFlowInput && !isAppInput}
|
||||
<ToggleButtonGroup bind:selected={oneOfSelected} class="mb-2">
|
||||
{#snippet children({ item })}
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if oneOfSelected && oneOfSchemas}
|
||||
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
||||
<div class="border">
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
noPreview
|
||||
bind:schema={oneOfSchemas[idx]}
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
editTab="inputEditor"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
||||
<div class="border">
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
noPreview
|
||||
bind:schema
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
editTab="inputEditor"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{@render children?.()}
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
||||
import type { DynamicSelect } from '$lib/utils'
|
||||
import { generateRandomString, type DynamicSelect } from '$lib/utils'
|
||||
interface Props {
|
||||
dndType?: string | undefined
|
||||
schema: Schema
|
||||
args?: Record<string, any>
|
||||
prettifyHeader?: boolean
|
||||
@@ -24,10 +23,11 @@
|
||||
isValid?: boolean
|
||||
noVariablePicker?: boolean
|
||||
helperScript?: DynamicSelect.HelperScript
|
||||
className?: string
|
||||
dndType?: string
|
||||
}
|
||||
|
||||
let {
|
||||
dndType = undefined,
|
||||
schema = $bindable(),
|
||||
args = $bindable(undefined),
|
||||
prettifyHeader = false,
|
||||
@@ -42,7 +42,9 @@
|
||||
diff = {},
|
||||
nestedClasses = '',
|
||||
isValid = $bindable(true),
|
||||
noVariablePicker = false
|
||||
noVariablePicker = false,
|
||||
className = '',
|
||||
dndType = generateRandomString()
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -55,14 +57,16 @@
|
||||
|
||||
let items = $state(computeItems())
|
||||
|
||||
let dragDisabled = $state(true)
|
||||
let dragDisabledState = $state(true)
|
||||
|
||||
function computeItems() {
|
||||
return (
|
||||
(schema?.order ?? Object.keys(schema?.properties ?? {}) ?? []).map((key) => ({
|
||||
id: key,
|
||||
value: key
|
||||
})) ?? []
|
||||
($state.snapshot(schema?.order) ?? Object.keys(schema?.properties ?? {}) ?? []).map(
|
||||
(key) => ({
|
||||
id: key,
|
||||
value: key
|
||||
})
|
||||
) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,23 +78,20 @@
|
||||
}
|
||||
|
||||
function handleConsider(e) {
|
||||
dragDisabled = false
|
||||
dragDisabledState = false
|
||||
const { items: newItems } = e.detail
|
||||
items = newItems
|
||||
items = $state.snapshot(newItems)
|
||||
}
|
||||
|
||||
function handleFinalize(e) {
|
||||
const { items: newItems } = e.detail
|
||||
|
||||
dragDisabled = true
|
||||
items = newItems
|
||||
|
||||
dragDisabledState = true
|
||||
items = $state.snapshot(newItems)
|
||||
const newOrder = items.map((item) => item.value)
|
||||
// console.log('handleFinalize', newOrder, e.detail)
|
||||
dispatch('reorder', newOrder)
|
||||
}
|
||||
$effect(() => {
|
||||
schema && dragDisabled && untrack(() => updateItems())
|
||||
schema && dragDisabledState && untrack(() => updateItems())
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -118,6 +119,7 @@
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
{helperScript}
|
||||
{className}
|
||||
bind:schema
|
||||
dndConfig={disableDnd
|
||||
? undefined
|
||||
@@ -125,7 +127,7 @@
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: dndType ?? 'top-level'
|
||||
type: dndType
|
||||
}}
|
||||
{items}
|
||||
{diff}
|
||||
|
||||
@@ -9,5 +9,4 @@ export type EditableSchemaWrapperProps = {
|
||||
customUi?: {
|
||||
noAddPopover?: boolean
|
||||
}
|
||||
onSchemaChange?: ({ schema }: { schema: Schema }) => void
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user