Merge branch 'main' into rf/snapshotMigrations

This commit is contained in:
Ruben Fiszel
2025-09-01 16:50:34 +00:00
176 changed files with 5407 additions and 3333 deletions
+61
View File
@@ -1,5 +1,66 @@
# 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)
### Features
* **aichat:** give advanced options tools to flow mode ([#6463](https://github.com/windmill-labs/windmill/issues/6463)) ([b26cea9](https://github.com/windmill-labs/windmill/commit/b26cea9d3e2f9a0acae335aad12206da491ac733))
* email triggers extra args in 'to' header ([#6476](https://github.com/windmill-labs/windmill/issues/6476)) ([ceb9150](https://github.com/windmill-labs/windmill/commit/ceb9150f43a0ae9f8579f1984e791f69e7a05366))
### Bug Fixes
* **cli:** specific items for file resource type ([#6464](https://github.com/windmill-labs/windmill/issues/6464)) ([2066a2a](https://github.com/windmill-labs/windmill/commit/2066a2ada2f3139474527f373dc505b7e61d5182))
* do not require locked for scheduled jobs ([41a8727](https://github.com/windmill-labs/windmill/commit/41a872725282ba4b78e8f9912bb1ac929b8557f7))
* **frontend:** ai agent step nits ([#6469](https://github.com/windmill-labs/windmill/issues/6469)) ([2b03133](https://github.com/windmill-labs/windmill/commit/2b03133b2245bd42f3c64b915d72dd3f62eb65a4))
* **frontend:** nats config conditional fields ([#6473](https://github.com/windmill-labs/windmill/issues/6473)) ([f90d444](https://github.com/windmill-labs/windmill/commit/f90d44469e0e4b462a5fa5160b64e99eef95c317))
* **go:** could not read Username for 'xyz': terminal prompts disabled ([#6478](https://github.com/windmill-labs/windmill/issues/6478)) ([5808840](https://github.com/windmill-labs/windmill/commit/5808840b78e94a0b39614f37161f9def347a5352))
* **go:** exec: "git": executable file not found ([#6475](https://github.com/windmill-labs/windmill/issues/6475)) ([475f405](https://github.com/windmill-labs/windmill/commit/475f405d0626f1c22309ee6a1b630472a89dbb30))
* save changes made in diff mode ([#6477](https://github.com/windmill-labs/windmill/issues/6477)) ([d9ca181](https://github.com/windmill-labs/windmill/commit/d9ca181b1d8d26c175ec2a05409c45daeab887a4))
## [1.535.0](https://github.com/windmill-labs/windmill/compare/v1.534.1...v1.535.0) (2025-08-25)
@@ -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"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path) DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
"query": "INSERT INTO deployment_metadata (workspace_id, path, flow_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path, flow_version) WHERE flow_version IS NOT NULL DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
"describe": {
"columns": [],
"parameters": {
@@ -14,5 +14,5 @@
},
"nullable": []
},
"hash": "434d8dfbc25cf7e92de51d763d3a2904ccc2e95ecc3d90b43a6394a7bb4d26ab"
"hash": "2367e7c0f7fbafe0971a187c0909617da55251e97180babf6ac9e8068f26d73d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM v2_job_queue, v2_job_runtime, v2_job_status, job_perms",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "4221d98d76f3cb32d6be581b0f63cf7578429009bee4f648e2c1bc3784fdbefc"
}
@@ -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"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM v2_job, v2_job_completed, job_result_stream, job_stats, job_logs, concurrency_key, log_file, metrics",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "807c920bff25f56b10e88900d879cf5e8484c147e457044d6b075323b163ebaa"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5) \n ON CONFLICT (workspace_id, path) DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
"query": "INSERT INTO deployment_metadata (workspace_id, path, script_hash, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5) \n ON CONFLICT (workspace_id, script_hash) WHERE script_hash IS NOT NULL DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
"describe": {
"columns": [],
"parameters": {
@@ -14,5 +14,5 @@
},
"nullable": []
},
"hash": "e77fcf4e0d58855542605d13177df61671334418820ca942b442adfab413cbae"
"hash": "8d119104337bf99e9aa9dcbac0a54154267a7db96cc0fb3ebaac95635e24da29"
}
@@ -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"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "VACUUM v2_job_queue, v2_job_runtime, v2_job_status",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "b9b38d63af3670d1f11d5cbb82a8008a9479bf4b3d7231371ebf26382ecde365"
}
@@ -0,0 +1,17 @@
{
"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, $4, 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",
"Text"
]
},
"nullable": []
},
"hash": "ce6f3e803909d55c19169c77d4111bffc0fc93032943369015971373f1f2af68"
}
@@ -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"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path) DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
"query": "INSERT INTO deployment_metadata (workspace_id, path, app_version, callback_job_ids, deployment_msg) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (workspace_id, path, app_version) WHERE app_version IS NOT NULL DO UPDATE SET callback_job_ids = $4, deployment_msg = $5",
"describe": {
"columns": [],
"parameters": {
@@ -14,5 +14,5 @@
},
"nullable": []
},
"hash": "f04632c3a8e0d7c5b48cdd26a99bb1dc5bd12df221f82405d663b8f15f5c0c3a"
"hash": "f37140fcdc721a8b199471b30c2baf124affa2eaf56c801c8dac3264c584f981"
}
+90 -118
View File
@@ -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",
@@ -1018,9 +1018,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",
]
@@ -1895,10 +1895,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",
@@ -2004,9 +2005,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.45"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2014,9 +2015,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.44"
version = "4.5.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41"
dependencies = [
"anstream",
"anstyle",
@@ -2105,9 +2106,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",
@@ -4449,9 +4450,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",
@@ -5130,7 +5131,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",
]
@@ -5141,7 +5142,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",
]
@@ -5240,6 +5241,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"
@@ -5833,9 +5840,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",
]
@@ -5863,7 +5870,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",
]
@@ -5929,7 +5936,7 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.10",
"regex-automata",
"regex-syntax 0.8.6",
]
@@ -6877,9 +6884,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"
@@ -6891,7 +6898,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.10",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -7811,7 +7818,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]]
@@ -7988,11 +7995,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]]
@@ -8581,16 +8588,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"
@@ -9293,12 +9290,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"
@@ -9877,9 +9868,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",
]
@@ -10253,9 +10244,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",
@@ -10264,7 +10255,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",
@@ -10273,9 +10264,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",
@@ -10294,16 +10285,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]]
@@ -10613,19 +10604,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"
@@ -10643,12 +10625,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"
@@ -13509,12 +13485,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",
@@ -13524,15 +13499,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",
@@ -14155,14 +14130,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",
@@ -14770,11 +14745,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]]
@@ -15129,7 +15104,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"axum",
@@ -15183,7 +15158,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"argon2",
@@ -15300,7 +15275,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15315,7 +15290,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"chrono",
"serde",
@@ -15328,7 +15303,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"axum",
@@ -15347,7 +15322,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15427,7 +15402,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"regex",
"serde",
@@ -15442,7 +15417,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"bytes",
@@ -15466,7 +15441,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15478,7 +15453,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15487,7 +15462,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15499,7 +15474,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15511,7 +15486,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"gosyn",
@@ -15523,7 +15498,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15535,7 +15510,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15547,7 +15522,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -15558,7 +15533,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15569,7 +15544,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15581,7 +15556,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15604,7 +15579,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15618,7 +15593,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15635,7 +15610,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15649,7 +15624,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15667,7 +15642,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"getrandom 0.2.16",
@@ -15692,7 +15667,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15702,7 +15677,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15735,7 +15710,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15745,7 +15720,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.535.0"
version = "1.538.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16325,13 +16300,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
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.535.0"
version = "1.538.0"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
]
[workspace.package]
version = "1.535.0"
version = "1.538.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1 +1 @@
15a7592ca66b93b9760d49e58b23c090ead06fe2
d16e52d570f10dfdabb04a9061fe7ebdbf5a2aa1
@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TRIGGER script_insert_trigger ON script;
@@ -0,0 +1,6 @@
-- Add up migration script here
CREATE TRIGGER script_insert_trigger
AFTER INSERT ON script
FOR EACH ROW
WHEN (NEW.lock IS NOT NULL)
EXECUTE FUNCTION notify_runnable_version_change('script');
@@ -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
View File
@@ -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",
];
+27 -3
View File
@@ -8,7 +8,7 @@ use std::{
atomic::{AtomicU16, Ordering},
Arc, Mutex,
},
time::Duration,
time::{Duration, Instant},
};
use chrono::{DateTime, NaiveDateTime, Utc};
@@ -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());
}
@@ -1530,6 +1532,20 @@ pub async fn monitor_db(
}
};
// run every hour
let vacuum_queue_f = async {
if server_mode && iteration.is_some() && iteration.as_ref().unwrap().should_run(60) {
if let Some(db) = conn.as_sql() {
let instant = Instant::now();
tracing::info!("vacuuming tables");
if let Err(e) = vacuuming_tables(&db).await {
tracing::error!("Error vacuuming v2_job: {:?}", e);
}
tracing::info!("vacuum tables done in {}s", instant.elapsed().as_secs());
}
}
};
let expired_items_f = async {
if server_mode && !initial_load {
if let Some(db) = conn.as_sql() {
@@ -1607,6 +1623,7 @@ pub async fn monitor_db(
expired_items_f,
zombie_jobs_f,
stale_jobs_f,
vacuum_queue_f,
expose_queue_metrics_f,
verify_license_key_f,
worker_groups_alerts_f,
@@ -1619,6 +1636,13 @@ pub async fn monitor_db(
);
}
async fn vacuuming_tables(db: &Pool<Postgres>) -> error::Result<()> {
sqlx::query!("VACUUM v2_job, v2_job_completed, job_result_stream, job_stats, job_logs, concurrency_key, log_file, metrics")
.execute(db)
.await?;
Ok(())
}
pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
let last_check = sqlx::query_scalar!(
"SELECT created_at FROM metrics WHERE id LIKE 'queue_count_%' ORDER BY created_at DESC LIMIT 1"
@@ -1874,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()
+30 -18
View File
@@ -4679,30 +4679,42 @@ mod job_payload {
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload(db: Pool<Postgres>) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let test = || async {
let result = RunJob::from(JobPayload::Dependencies {
path: "f/system/hello".to_string(),
hash: ScriptHash(123412),
language: ScriptLang::Deno,
dedicated_worker: None,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
let result = RunJob::from(JobPayload::Dependencies {
path: "f/system/hello".to_string(),
hash: ScriptHash(123412),
language: ScriptLang::Deno,
dedicated_worker: None,
})
.run_until_complete(&db, port)
.await
.json_result()
.unwrap();
assert_eq!(
result.get("status").unwrap(),
&json!("Successful lock file generation")
);
};
test_for_versions(VERSION_FLAGS.iter().cloned(), test).await;
assert_eq!(
result.get("status").unwrap(),
&json!("Successful lock file generation")
);
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload_min_1_427(db: Pool<Postgres>) {
*MIN_VERSION_IS_AT_LEAST_1_427.write().await = true;
test_dependencies_payload(db).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload_min_1_432(db: Pool<Postgres>) {
*MIN_VERSION_IS_AT_LEAST_1_432.write().await = true;
test_dependencies_payload(db).await;
}
#[sqlx::test(fixtures("base", "hello"))]
async fn test_dependencies_payload_min_1_440(db: Pool<Postgres>) {
*MIN_VERSION_IS_AT_LEAST_1_440.write().await = true;
test_dependencies_payload(db).await;
}
// Just test that deploying a flow work as expected.
+11 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.535.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:
+4
View File
@@ -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)
};
+5 -4
View File
@@ -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"))]
+1
View File
@@ -592,6 +592,7 @@ pub async fn transform_json_value<'c>(
job.flow_step_id.clone(),
job.root_job.map(|x| x.to_string()),
Some(job.scheduled_for.clone()),
None,
)
.await;
+74 -17
View File
@@ -28,16 +28,13 @@ use axum::{
};
use hyper::StatusCode;
use itertools::Itertools;
use quick_cache::sync::Cache;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::value::RawValue;
use sql_builder::prelude::*;
use sqlx::{FromRow, Postgres, Transaction};
use std::{
collections::{hash_map::DefaultHasher, HashMap},
hash::{Hash, Hasher},
sync::Arc,
};
use std::{collections::HashMap, sync::Arc};
use windmill_audit::audit_oss::audit_log;
use windmill_audit::ActionKind;
use windmill_worker::process_relative_imports;
@@ -45,6 +42,8 @@ use windmill_worker::process_relative_imports;
use windmill_common::{
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType},
error::to_anyhow,
scripts::hash_script,
utils::WarnAfterExt,
worker::CLOUD_HOSTED,
};
@@ -378,12 +377,6 @@ async fn get_top_hub_scripts(
Ok::<_, Error>((status_code, headers, response))
}
fn hash_script(ns: &NewScript) -> i64 {
let mut dh = DefaultHasher::new();
ns.hash(&mut dh);
dh.finish() as i64
}
async fn create_snapshot_script(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -1369,8 +1362,9 @@ async fn toggle_workspace_error_handler(
async fn get_tokened_raw_script_by_path(
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, token, path)): Path<(String, String, StripPath)>,
Extension(cache): Extension<Arc<AuthCache>>,
Path((w_id, token, path)): Path<(String, String, StripPath)>,
Query(query): Query<RawScriptByPathQuery>,
) -> Result<String> {
let authed = cache
.get_authed(Some(w_id.clone()), &token)
@@ -1381,6 +1375,7 @@ async fn get_tokened_raw_script_by_path(
Extension(user_db),
Extension(db),
Path((w_id, path)),
Query(query),
)
.await;
}
@@ -1389,13 +1384,21 @@ async fn get_empty_ts_script_by_path() -> String {
return String::new();
}
#[derive(Deserialize)]
struct RawScriptByPathQuery {
// used to make cache immutable with respect to importer
cache_key: Option<String>,
// used specifically for python to cache folders on import success to avoid extra db calls on package fetch
cache_folders: Option<bool>,
}
async fn raw_script_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(query): Query<RawScriptByPathQuery>,
) -> Result<String> {
raw_script_by_path_internal(path, user_db, db, authed, w_id, false).await
raw_script_by_path_internal(path, user_db, db, authed, w_id, false, query).await
}
async fn raw_script_by_path_unpinned(
@@ -1403,8 +1406,9 @@ async fn raw_script_by_path_unpinned(
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Query(query): Query<RawScriptByPathQuery>,
) -> Result<String> {
raw_script_by_path_internal(path, user_db, db, authed, w_id, true).await
raw_script_by_path_internal(path, user_db, db, authed, w_id, true, query).await
}
lazy_static::lazy_static! {
@@ -1412,6 +1416,12 @@ lazy_static::lazy_static! {
std::env::var("DEBUG_RAW_SCRIPT_ENDPOINTS").is_ok();
}
lazy_static::lazy_static! {
pub static ref RAW_SCRIPT_CACHE: Cache<String, String> = Cache::new(1000);
pub static ref CACHE_FOLDERS_PATH: Cache<String, i64> = Cache::new(1000);
}
async fn raw_script_by_path_internal(
path: StripPath,
user_db: UserDB,
@@ -1419,9 +1429,20 @@ async fn raw_script_by_path_internal(
authed: ApiAuthed,
w_id: String,
unpin: bool,
query: RawScriptByPathQuery,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("scripts:read:{}", path))?;
let cache_path = query
.cache_key
.map(|x| format!("{w_id}:{path}:{x}{}", if unpin { ":unpinned" } else { "" }));
if let Some(cache_path) = cache_path.clone() {
let cached_content = RAW_SCRIPT_CACHE.get(&cache_path);
if let Some(cached_content) = cached_content {
return Ok(cached_content);
}
}
if !path.ends_with(".py")
&& !path.ends_with(".ts")
&& !path.ends_with(".go")
@@ -1439,6 +1460,27 @@ async fn raw_script_by_path_internal(
.trim_end_matches(".ts")
.trim_end_matches(".go")
.trim_end_matches(".sh");
// folder cache is only useful for python given it needs to recuse over all intermediate folders to find the package.
// When a script exists in a folder, we can cache the fact that the folder exists to avoid extra db calls.
let mut split_path = path.split("/").collect::<Vec<&str>>();
let folder_path = if query.cache_folders.is_some() && split_path.len() > 2 {
Some(format!("{w_id}:{path}/"))
} else {
None
};
let has_folder_cache = folder_path.is_some();
if let Some(cache_folders) = folder_path {
let cached_content = CACHE_FOLDERS_PATH.get(&cache_folders);
if let Some(cached_ts) = cached_content {
if cached_ts >= chrono::Utc::now().timestamp() - 300 {
// 5 minutes
return Ok("WINDMILL_IS_FOLDER".to_string());
}
}
}
let mut tx = user_db.begin(&authed).await?;
let content_o = sqlx::query_scalar!(
@@ -1447,6 +1489,7 @@ async fn raw_script_by_path_internal(
w_id
)
.fetch_optional(&mut *tx)
.warn_after_seconds(5)
.await?;
tx.commit().await?;
@@ -1457,6 +1500,7 @@ async fn raw_script_by_path_internal(
w_id
)
.fetch_one(&db)
.warn_after_seconds(5)
.await?
.unwrap_or(false);
@@ -1490,11 +1534,24 @@ async fn raw_script_by_path_internal(
let content = not_found_if_none(content_o, "Script", path)?;
if unpin {
return Ok(remove_pinned_imports(&content)?);
let content = if unpin {
remove_pinned_imports(&content)?
} else {
return Ok(content);
content
};
if has_folder_cache {
while split_path.len() >= 2 {
split_path.pop();
let npath = split_path.join("/");
CACHE_FOLDERS_PATH.insert(format!("{w_id}:{npath}/"), chrono::Utc::now().timestamp());
}
}
if let Some(cache_path) = cache_path {
RAW_SCRIPT_CACHE.insert(cache_path, content.clone());
}
Ok(content)
}
async fn exists_script_by_path(
+13 -28
View File
@@ -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
+7 -1
View File
@@ -24,7 +24,11 @@ use serde_json::Value;
use windmill_audit::audit_oss::{audit_log, AuditAuthorable};
use windmill_audit::ActionKind;
use windmill_common::{
db::UserDB, error::{Error, JsonResult, Result}, utils::{not_found_if_none, paginate, Pagination, StripPath}, variables::{
db::UserDB,
error::{Error, JsonResult, Result},
scripts::ScriptHash,
utils::{not_found_if_none, paginate, Pagination, StripPath, WarnAfterExt},
variables::{
build_crypt, get_reserved_variables, ContextualVariable, CreateVariable, ListableVariable,
},
worker::CLOUD_HOSTED,
@@ -74,6 +78,7 @@ async fn list_contextual_variables(
Some("c".to_string()),
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
Some(chrono::offset::Utc::now()),
Some(ScriptHash(1234567890)),
)
.await
.to_vec(),
@@ -693,6 +698,7 @@ pub async fn get_value_internal<'c>(
LEFT JOIN account ON variable.account = account.id WHERE variable.path = $1 AND variable.workspace_id = $2", path, w_id
)
.fetch_optional(&mut *tx)
.warn_after_seconds(5)
.await?;
let variable = if let Some(variable) = variable_o {
+13 -3
View File
@@ -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",
+11 -1
View File
@@ -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(());
+6
View File
@@ -649,3 +649,9 @@ pub struct HubScript {
pub schema: Box<serde_json::value::RawValue>,
pub summary: Option<String>,
}
pub fn hash_script(ns: &NewScript) -> i64 {
let mut dh = std::hash::DefaultHasher::new();
ns.hash(&mut dh);
dh.finish() as i64
}
+6 -2
View File
@@ -265,14 +265,18 @@ pub fn create_directory_sync(directory_path: &str) {
.expect("could not create dir");
}
#[track_caller]
pub fn not_found_if_none<T, U: AsRef<str>>(opt: Option<T>, kind: &str, name: U) -> Result<T> {
if let Some(o) = opt {
Ok(o)
} else {
let loc = Location::caller();
Err(Error::NotFound(format!(
"{} not found at name {}",
"{} not found at name {} ({}:{})",
kind,
name.as_ref()
name.as_ref(),
loc.file().split("/").last().unwrap_or_default(),
loc.line()
)))
}
}
+44 -6
View File
@@ -7,6 +7,8 @@
*/
use crate::error;
use crate::scripts::ScriptHash;
use crate::utils::WarnAfterExt;
use crate::worker::Connection;
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
use chrono::{SecondsFormat, Utc};
@@ -77,13 +79,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(
@@ -106,8 +133,10 @@ pub async fn get_workspace_key(w_id: &str, db: &DB) -> crate::error::Result<Stri
w_id
)
.fetch_one(db)
.warn_after_seconds(5)
.await
.map_err(|e| crate::Error::internal_err(format!("fetching workspace key: {e:#}")))?;
Ok(key)
}
@@ -164,6 +193,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(
@@ -181,6 +212,7 @@ pub async fn get_reserved_variables(
step_id: Option<String>,
root_flow_id: Option<String>,
scheduled_for: Option<chrono::DateTime<Utc>>,
runnable_id: Option<ScriptHash>,
) -> Vec<ContextualVariable> {
let state_path = {
let trigger = if schedule_path.is_some() {
@@ -336,7 +368,13 @@ pub async fn get_reserved_variables(
ContextualVariable {
name: "WM_WORKER_GROUP".to_string(),
value: WORKER_GROUP.clone(),
description: "name of the worker group the job is running on".to_string(),
description: "Name of the worker group the job is running on".to_string(),
is_custom: false,
},
ContextualVariable {
name: "WM_RUNNABLE_ID".to_string(),
value: runnable_id.map(|x| x.to_string()).unwrap_or_else(|| "".to_string()),
description: "Hash of the script. Useful as cache key for cache that should be runnable specific.".to_string(),
is_custom: false,
},
].into_iter().chain(custom_envs.into_iter().map(|(name, value)| ContextualVariable {
+34 -9
View File
@@ -86,22 +86,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)]
+51 -39
View File
@@ -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."
)));
}
}
}
}
+1 -1
View File
@@ -156,7 +156,7 @@ pub async fn push_scheduled_job<'c>(
&mut *tx,
&schedule.workspace_id,
&schedule.script_path,
true,
false,
)
.await?;
+28 -10
View File
@@ -2,6 +2,7 @@ import sys
import os
from importlib.abc import MetaPathFinder, Loader
from importlib.machinery import ModuleSpec, SourceFileLoader
import urllib.response
class WindmillLoader(Loader):
@@ -27,7 +28,15 @@ class WindmillFinder(MetaPathFinder):
if l <= 2:
return ModuleSpec(name, WindmillLoader(name))
elif l > 2:
script_path = "/".join(splitted)
folder = os.getcwd() + "/tmp/" + "/".join(splitted[:-1])
fullpath = folder + "/" + splitted[-1] + ".py"
if os.path.exists(fullpath):
return ModuleSpec(name, SourceFileLoader(name, fullpath))
import urllib.parse
import urllib.request
@@ -35,20 +44,29 @@ class WindmillFinder(MetaPathFinder):
"Authorization": f"Bearer {os.environ.get('WM_TOKEN')}",
"User-Agent": "windmill/beta"
}
url = f"{os.environ.get('BASE_INTERNAL_URL')}/api/w/{os.environ.get('WM_WORKSPACE')}/scripts/raw/p/{script_path}.py"
query_params = "?cache_folders=true"
runnable_id = os.environ.get('WM_RUNNABLE_ID')
if runnable_id:
query_params += f"&cache_key={runnable_id}"
url = f"{os.environ.get('BASE_INTERNAL_URL')}/api/w/{os.environ.get('WM_WORKSPACE')}/scripts/raw/p/{script_path}.py{query_params}"
req = urllib.request.Request(url, None, headers)
try:
with urllib.request.urlopen(req) as response:
r = response.read().decode("utf-8")
folder = os.getcwd() + "/tmp/" + "/".join(splitted[:-1])
fullpath = folder + "/" + splitted[-1] + ".py"
os.makedirs(folder, exist_ok=True)
with open(fullpath, "w+") as f:
f.write(r)
return ModuleSpec(name, SourceFileLoader(name, fullpath))
except:
# raise ImportError(f"Script {script_path} not found")
os.makedirs(folder, exist_ok=True)
r = response.read().decode("utf-8")
if r == "WINDMILL_IS_FOLDER":
return ModuleSpec(name, WindmillLoader(name))
with open(fullpath, "w+") as f:
f.write(r)
return ModuleSpec(name, SourceFileLoader(name, fullpath))
except urllib.error.HTTPError as e:
if e.code != 404:
print(f"Error fetching script {script_path}: HTTP {e.code} - {e.reason}")
return ModuleSpec(name, WindmillLoader(name))
except Exception as e:
print(f"Error fetching script {script_path}: {e}")
return ModuleSpec(name, WindmillLoader(name))
@@ -1589,6 +1589,7 @@ pub async fn start_worker(
None,
None,
None,
None,
)
.await;
let context_envs = build_envs_map(context.to_vec()).await;
+4 -2
View File
@@ -464,6 +464,7 @@ pub async fn get_reserved_variables(
job.flow_step_id.clone(),
job.flow_innermost_root_job.clone().map(|x| x.to_string()),
Some(job.scheduled_for.clone()),
job.runnable_id,
)
.await
.to_vec();
@@ -680,11 +681,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;
@@ -544,6 +544,7 @@ pub async fn start_worker(
None,
None,
None,
None,
)
.await;
let context_envs = build_envs_map(context.to_vec()).await;
@@ -571,6 +571,8 @@ pub async fn install_go_dependencies(
child_cmd
.current_dir(job_dir)
.env_clear()
.env("HOME", HOME_ENV.as_str())
.env("PATH", PATH_ENV.as_str())
.env("GOPATH", {
#[cfg(unix)]
{
@@ -584,6 +586,18 @@ pub async fn install_go_dependencies(
.args(vec!["mod", mod_command])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(ref goprivate) = *GOPRIVATE {
child_cmd.env("GOPRIVATE", goprivate);
}
// TODO: Remove if no incidents reported
if !std::env::var("WMDEBUG_NO_GOPROXY_ON_TIDY").ok().is_some() {
if let Some(ref goproxy) = *GOPROXY {
child_cmd.env("GOPROXY", goproxy);
}
}
// If annotation used we want to call tidy with special flag to pin go to 1.22
// The reason for this that at some point we had to jump from go 1.22 to 1.25 and this addds backward compatibility.
if anns.go1_22_compat && mod_command == "tidy" {
+1 -1
View File
@@ -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 {
""
},
+3 -1
View File
@@ -63,7 +63,9 @@ mod worker_lockfiles;
mod worker_utils;
pub use worker::*;
pub use worker_lockfiles::process_relative_imports;
pub use worker_lockfiles::{
process_relative_imports, trigger_dependents_to_recompute_dependencies,
};
pub use result_processor::handle_job_error;
@@ -2145,6 +2145,7 @@ pub async fn start_worker(
None,
None,
None,
None,
)
.await
.to_vec();
@@ -2264,6 +2265,7 @@ for line in sys.stdin:
None,
None,
None,
None,
)
.await;
+2 -4
View File
@@ -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
+138 -17
View File
@@ -17,7 +17,7 @@ use windmill_common::error::Result;
use windmill_common::flows::{FlowModule, FlowModuleValue, FlowNodeId};
use windmill_common::get_latest_deployed_hash_for_path;
use windmill_common::jobs::JobPayload;
use windmill_common::scripts::ScriptHash;
use windmill_common::scripts::{hash_script, NewScript, ScriptHash};
#[cfg(feature = "python")]
use windmill_common::worker::PythonAnnotations;
use windmill_common::worker::{to_raw_value, to_raw_value_owned, write_file, Connection};
@@ -38,6 +38,11 @@ use windmill_parser_py_imports::parse_relative_imports;
use windmill_parser_ts::parse_expr_for_imports;
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob, PushIsolationLevel};
lazy_static::lazy_static! {
// TODO: To be removed in future versions
static ref WMDEBUG_NO_HASH_CHANGE_ON_DJ: bool = std::env::var("WMDEBUG_NO_HASH_CHANGE_ON_DJ").is_ok();
}
use crate::common::OccupancyMetrics;
use crate::csharp_executor::generate_nuget_lockfile;
@@ -251,6 +256,7 @@ pub async fn handle_dependency_job(
.is_some_and(|y| y.to_string().as_str() == "true")
})
.unwrap_or(false);
let npm_mode = if job
.script_lang
.as_ref()
@@ -338,30 +344,140 @@ pub async fn handle_dependency_job(
));
}
let hash = job.runnable_id.unwrap_or(ScriptHash(0));
let current_hash = job.runnable_id.unwrap_or(ScriptHash(0));
let w_id = &job.workspace_id;
sqlx::query!(
"UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
&content,
&hash.0,
w_id
)
.execute(db)
.await?;
// `lock` has been updated; invalidate the cache.
cache::script::invalidate(hash);
let (deployment_message, parent_path) =
get_deployment_msg_and_parent_path_from_args(job.args.clone());
let script_info = sqlx::query_as::<_, windmill_common::scripts::Script>(
"SELECT * FROM script WHERE hash = $1 AND workspace_id = $2",
)
.bind(&current_hash.0)
.bind(w_id)
.fetch_one(db)
.await?;
// DependencyJob can be triggered only from 2 places:
// 1. create_script function in windmill-api/src/scripts.rs
// 2. trigger_dependents_to_recompute_dependencies (in this file)
//
// First will **always** produce script with null in `lock`
// where Second will **always** do with lock being not null
let deployed_hash = if script_info.lock.is_some() && !*WMDEBUG_NO_HASH_CHANGE_ON_DJ {
let mut tx = db.begin().await?;
// This entire section exists to solve following problem:
//
// 2 workers, one script that depend on another in python
// run the original script on both workers
// you update the dependenecy of a relative import,
// run it again until you ran it on both, normally it should fail on one of those
//
// It happens because every worker has cached their own script versions.
// However usual dependency job does not update hash of the script (and cache is keyed by the hash).
// This logical branch will create new script which will update the hash and automatically invalidate cache.
//
// IMPORTANT: This will **only** be triggered by another DependencyJob. It will never be triggered by script (re)deployement
let ns = NewScript {
path: script_info.path,
parent_hash: Some(current_hash),
summary: script_info.summary,
description: script_info.description,
content: script_info.content,
schema: script_info.schema,
is_template: Some(script_info.is_template),
// TODO: Make it either None everywhere (particularely when raw reqs are calculated)
// Or handle this case and conditionally make Some (only with raw reqs)
lock: None,
language: script_info.language,
kind: Some(script_info.kind),
tag: script_info.tag,
draft_only: script_info.draft_only,
envs: script_info.envs,
concurrent_limit: script_info.concurrent_limit,
concurrency_time_window_s: script_info.concurrency_time_window_s,
cache_ttl: script_info.cache_ttl,
dedicated_worker: script_info.dedicated_worker,
ws_error_handler_muted: script_info.ws_error_handler_muted,
priority: script_info.priority,
timeout: script_info.timeout,
delete_after_use: script_info.delete_after_use,
restart_unless_cancelled: script_info.restart_unless_cancelled,
deployment_message: deployment_message.clone(),
concurrency_key: script_info.concurrency_key,
visible_to_runner_only: script_info.visible_to_runner_only,
no_main_func: script_info.no_main_func,
codebase: script_info.codebase,
has_preprocessor: script_info.has_preprocessor,
on_behalf_of_email: script_info.on_behalf_of_email,
assets: script_info.assets,
};
let new_hash = hash_script(&ns);
sqlx::query!("
INSERT INTO script
(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)
SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, \
content, created_by, schema, is_template, extra_perms, $4, 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
FROM script WHERE hash = $2 AND workspace_id = $3;
",
new_hash, current_hash.0, w_id, &content).execute(db).await?;
// Archive current
sqlx::query!(
"UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2",
current_hash.0,
w_id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
ScriptHash(new_hash)
} else {
// We do not create new row for this update
// That means we can keep current hash and just update lock
sqlx::query!(
"UPDATE script SET lock = $1 WHERE hash = $2 AND workspace_id = $3",
&content,
&current_hash.0,
w_id
)
.execute(db)
.await?;
// `lock` has been updated; invalidate the cache.
// Since only worker that ran this Dependency Job has the cache
// we do not need to think about invalidating cache for other workers.
cache::script::invalidate(current_hash);
if *WMDEBUG_NO_HASH_CHANGE_ON_DJ {
tracing::warn!("WMDEBUG_NO_HASH_CHANGE_ON_DJ usually should not be used. Behavior might be unstable. Please contact Windmill Team for support.")
}
current_hash
};
if let Err(e) = handle_deployment_metadata(
&job.permissioned_as_email,
&job.created_by,
&db,
&w_id,
DeployedObject::Script {
hash,
hash: deployed_hash,
path: script_path.to_string(),
parent_path: parent_path.clone(),
},
@@ -502,7 +618,7 @@ pub async fn process_relative_imports(
Ok(())
}
async fn trigger_dependents_to_recompute_dependencies(
pub async fn trigger_dependents_to_recompute_dependencies(
w_id: &str,
script_path: &str,
deployment_message: Option<String>,
@@ -535,6 +651,10 @@ async fn trigger_dependents_to_recompute_dependencies(
args.insert("deployment_message".to_string(), to_raw_value(&dm));
}
if let Some(ref p_path) = parent_path {
// NOTE:
// it's not used but maybe one day it will be useful. allows more back-compatibility for the workers when we need it
// also very useful for debugging/observability
// it adds that information to the job args so you can see from the runs page
args.insert("common_dependency_path".to_string(), to_raw_value(&p_path));
}
@@ -542,6 +662,7 @@ async fn trigger_dependents_to_recompute_dependencies(
"already_visited".to_string(),
to_raw_value(&already_visited),
);
let kind = s.importer_kind.clone().unwrap_or_default();
let job_payload = if kind == "script" {
let r = get_latest_deployed_hash_for_path(db, w_id, s.importer_path.as_str()).await;
@@ -577,7 +698,7 @@ async fn trigger_dependents_to_recompute_dependencies(
Ok(Some(version)) => JobPayload::FlowDependencies {
path: s.importer_path.clone(),
dedicated_worker: None,
version: version,
version,
},
Ok(None) => {
tracing::error!(
+1 -1
View File
@@ -308,7 +308,7 @@ pub(crate) async fn queue_vacuum(conn: &Connection, worker_name: &str, hostname:
tokio::task::spawn(
(async move {
tracing::info!(worker = %worker_name, hostname = %hostname, "vacuuming queue");
if let Err(e) = sqlx::query!("VACUUM v2_job_queue, v2_job_runtime, v2_job_status")
if let Err(e) = sqlx::query!("VACUUM v2_job_queue, v2_job_runtime, v2_job_status, job_perms")
.execute(&db2)
.await
{
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.535.0";
export const VERSION = "v1.538.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+38 -6
View File
@@ -11,6 +11,8 @@ import { colors, Command, log, SEP, Table } from "../../../deps.ts";
import * as wmill from "../../../gen/services.gen.ts";
import { Resource } from "../../../gen/types.gen.ts";
import { readInlinePathSync } from "../../utils/utils.ts";
import { isBranchSpecificFile } from "../../core/specific_items.ts";
import { getCurrentGitBranch } from "../../utils/git.ts";
export interface ResourceFile {
value: any;
@@ -23,7 +25,8 @@ export async function pushResource(
workspace: string,
remotePath: string,
resource: ResourceFile | Resource | undefined,
localResource: ResourceFile
localResource: ResourceFile,
originalLocalPath?: string
): Promise<void> {
remotePath = removeType(remotePath, "resource");
try {
@@ -35,21 +38,49 @@ export async function pushResource(
// flow doesn't exist
}
if (localResource.value["content"]?.startsWith("!inline ")) {
const basePath = localResource.value["content"].split(" ")[1];
localResource.value["content"] = readInlinePathSync(basePath);
}
// Helper function to resolve inline content
const resolveInlineContent = async () => {
if (localResource.value["content"]?.startsWith("!inline ")) {
const basePath = localResource.value["content"].split(" ")[1];
// If we're processing a branch-specific metadata file, read from branch-specific resource file
let pathToRead = basePath;
if (originalLocalPath && isBranchSpecificFile(originalLocalPath)) {
const currentBranch = getCurrentGitBranch();
if (currentBranch) {
// Directly construct branch-specific resource file path
const resourcePathSegments = basePath.split(".");
if (resourcePathSegments.length >= 4 && resourcePathSegments[resourcePathSegments.length - 3] === "resource" && resourcePathSegments[resourcePathSegments.length - 2] === "file") {
const fileBaseParts = resourcePathSegments.slice(0, -3);
const fileExt = resourcePathSegments.slice(-3);
pathToRead = [...fileBaseParts, currentBranch, ...fileExt].join(".");
}
}
}
localResource.value["content"] = readInlinePathSync(pathToRead);
}
};
if (resource) {
if (isSuperset(localResource, resource)) {
return;
}
// Only resolve inline content if we're actually updating
await resolveInlineContent();
await wmill.updateResource({
workspace: workspace,
path: remotePath.replaceAll(SEP, "/"),
requestBody: { ...localResource },
});
} else {
// New resource - resolve inline content
await resolveInlineContent();
if (localResource.is_oauth) {
log.info(
colors.yellow(
@@ -89,7 +120,8 @@ async function push(opts: PushOptions, filePath: string, remotePath: string) {
workspace.workspaceId,
remotePath,
undefined,
parseFromFile(filePath)
parseFromFile(filePath),
filePath // Pass the local file path for branch-specific inline content resolution
);
log.info(colors.bold.underline.green(`Resource ${remotePath} pushed`));
}
+19 -5
View File
@@ -15,6 +15,8 @@ import {
} from "../../../deps.ts";
import { deepEqual } from "../../utils/utils.ts";
import * as wmill from "../../../gen/services.gen.ts";
import * as specificItems from "../../core/specific_items.ts";
import { getCurrentGitBranch } from "../../utils/git.ts";
import {
defaultScriptMetadata,
@@ -102,12 +104,24 @@ async function push(opts: PushOptions, filePath: string) {
export async function findResourceFile(path: string) {
const splitPath = path.split(".");
const contentBasePathJSON = splitPath[0] + "." + splitPath[1] + ".json";
const contentBasePathYAML = splitPath[0] + "." + splitPath[1] + ".yaml";
let contentBasePathJSON = splitPath[0] + "." + splitPath[1] + ".json";
let contentBasePathYAML = splitPath[0] + "." + splitPath[1] + ".yaml";
// Check for branch-specific metadata files first
const currentBranch = getCurrentGitBranch();
const candidates = [contentBasePathJSON, contentBasePathYAML];
if (currentBranch) {
// Add branch-specific candidates at the beginning (higher priority)
const branchSpecificJSON = specificItems.toBranchSpecificPath(contentBasePathJSON, currentBranch);
const branchSpecificYAML = specificItems.toBranchSpecificPath(contentBasePathYAML, currentBranch);
candidates.unshift(branchSpecificJSON, branchSpecificYAML);
}
const validCandidates = (
await Promise.all(
[contentBasePathJSON, contentBasePathYAML].map((x) => {
candidates.map((x) => {
return Deno.stat(x)
.catch(() => undefined)
.then((x) => x?.isFile)
@@ -580,7 +594,7 @@ export function filePathExtensionFromContentType(
return ".java";
} else if (language === "ruby") {
return ".rb";
// for related places search: ADD_NEW_LANG
// for related places search: ADD_NEW_LANG
} else {
throw new Error("Invalid language: " + language);
}
@@ -611,7 +625,7 @@ export const exts = [
".playbook.yml",
".java",
".rb"
// for related places search: ADD_NEW_LANG
// for related places search: ADD_NEW_LANG
];
export function removeExtensionToPath(path: string): string {
+33 -4
View File
@@ -1930,11 +1930,21 @@ export async function push(
await Deno.readTextFile(resourceFilePath)
);
// For branch-specific resources, push to the base path on the workspace server
// This ensures branch-specific files are stored with their base names in the workspace
let serverPath = resourceFilePath;
const currentBranch = getCurrentGitBranch();
if (currentBranch && isBranchSpecificFile(resourceFilePath)) {
serverPath = fromBranchSpecificPath(resourceFilePath, currentBranch);
}
await pushResource(
workspace.workspaceId,
resourceFilePath,
serverPath,
undefined,
newObj
newObj,
resourceFilePath
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
@@ -1945,6 +1955,12 @@ export async function push(
const oldObj = parseFromPath(change.path, change.before);
const newObj = parseFromPath(change.path, change.after);
// Check if this is a branch-specific item and get the original branch-specific path
let originalBranchSpecificPath: string | undefined;
if (specificItems && isSpecificItem(change.path, specificItems)) {
originalBranchSpecificPath = getBranchSpecificPath(change.path, specificItems);
}
await pushObj(
workspace.workspaceId,
change.path,
@@ -1952,7 +1968,8 @@ export async function push(
newObj,
opts.plainSecrets ?? false,
alreadySynced,
opts.message
opts.message,
originalBranchSpecificPath
);
if (stateTarget) {
@@ -1986,6 +2003,17 @@ export async function push(
);
}
const obj = parseFromPath(change.path, change.content);
// Determine the actual local file path for this change
// For branch-specific items, we read from branch-specific files but push to base server paths
let localFilePath = change.path;
if (specificItems && isSpecificItem(change.path, specificItems)) {
const branchSpecificPath = getBranchSpecificPath(change.path, specificItems);
if (branchSpecificPath) {
localFilePath = branchSpecificPath;
}
}
await pushObj(
workspace.workspaceId,
change.path,
@@ -1993,7 +2021,8 @@ export async function push(
obj,
opts.plainSecrets ?? false,
[],
opts.message
opts.message,
localFilePath // Pass the actual local file path
);
if (stateTarget) {
+21 -14
View File
@@ -15,7 +15,10 @@ import {
isSuperset,
parseFromFile,
removeType,
TRIGGER_TYPES,
} from "../../types.ts";
import { fromBranchSpecificPath, isBranchSpecificFile } from "../../core/specific_items.ts";
import { getCurrentGitBranch } from "../../utils/git.ts";
import { requireLogin } from "../../core/auth.ts";
import { validatePath, resolveWorkspace } from "../../core/context.ts";
@@ -222,25 +225,29 @@ async function list(opts: GlobalOptions) {
}
function checkIfValidTrigger(kind: string | undefined): kind is TriggerType {
if (
kind &&
[
"http",
"websocket",
"kafka",
"nats",
"postgres",
"mqtt",
"sqs",
"gcp",
].includes(kind)
) {
if (kind && (TRIGGER_TYPES as readonly string[]).includes(kind)) {
return true;
} else {
return false;
}
}
function extractTriggerKindFromPath(filePath: string): string | undefined {
let pathToAnalyze = filePath;
// If this is a branch-specific file, convert it to the base path first
if (isBranchSpecificFile(filePath)) {
const currentBranch = getCurrentGitBranch();
if (currentBranch) {
pathToAnalyze = fromBranchSpecificPath(filePath, currentBranch);
}
}
// Now extract trigger type from the base path: "something.kafka_trigger.yaml" -> "kafka"
const triggerMatch = pathToAnalyze.match(/\.(\w+)_trigger\.yaml$/);
return triggerMatch ? triggerMatch[1] : undefined;
}
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
@@ -256,7 +263,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
console.log(colors.bold.yellow("Pushing trigger..."));
const triggerKind = filePath.split(".")[1].split("_")[0];
const triggerKind = extractTriggerKindFromPath(filePath);
if (!checkIfValidTrigger(triggerKind)) {
throw new Error("Invalid trigger kind: " + triggerKind);
}
+4
View File
@@ -44,6 +44,7 @@ export interface SyncOptions {
commonSpecificItems?: {
variables?: string[];
resources?: string[];
triggers?: string[];
};
} & {
[branchName: string]: SyncOptions & {
@@ -54,6 +55,7 @@ export interface SyncOptions {
specificItems?: {
variables?: string[];
resources?: string[];
triggers?: string[];
};
};
};
@@ -62,6 +64,7 @@ export interface SyncOptions {
commonSpecificItems?: {
variables?: string[];
resources?: string[];
triggers?: string[];
};
} & {
[branchName: string]: SyncOptions & {
@@ -72,6 +75,7 @@ export interface SyncOptions {
specificItems?: {
variables?: string[];
resources?: string[];
triggers?: string[];
};
};
};
+20 -13
View File
@@ -109,15 +109,11 @@ async function tryResolveWorkspace(
return { isError: false, value: e };
}
const defaultWorkspace = await getActiveWorkspace(opts);
if (!defaultWorkspace) {
return {
isError: true,
error: colors.red.underline("No workspace given and no default set."),
};
}
return { isError: false, value: defaultWorkspace };
// Only check for explicit workspace, don't fallback to active workspace here
return {
isError: true,
error: colors.red.underline("No explicit workspace given."),
};
}
async function tryResolveBranchWorkspace(
@@ -259,6 +255,9 @@ async function tryResolveBranchWorkspace(
export async function resolveWorkspace(
opts: GlobalOptions
): Promise<Workspace> {
const cache = (opts as any).__secret_workspace;
if (cache) return cache;
if (opts.baseUrl) {
if (opts.workspace && opts.token) {
let normalizedBaseUrl: string;
@@ -328,20 +327,28 @@ export async function resolveWorkspace(
}
}
// Try explicit workspace flag first (should override branch-based resolution)
// Try explicit workspace flag first (highest priority)
const res = await tryResolveWorkspace(opts);
if (!res.isError) {
return res.value;
}
// Fall back to branch-based resolution if no explicit workspace
// Try branch-based resolution (medium priority)
const branchWorkspace = await tryResolveBranchWorkspace(opts);
if (branchWorkspace) {
(opts as any).__secret_workspace = branchWorkspace;
return branchWorkspace;
}
// If both failed, show the original error from explicit workspace resolution
log.info(colors.red.bold(res.error));
// Fall back to active workspace (lowest priority)
const activeWorkspace = await getActiveWorkspace(opts);
if (activeWorkspace) {
(opts as any).__secret_workspace = activeWorkspace;
return activeWorkspace;
}
// If everything failed, show error
log.info(colors.red.bold("No workspace given and no default set."));
return Deno.exit(-1);
}
+112 -16
View File
@@ -1,10 +1,59 @@
import { minimatch } from "../../deps.ts";
import { getCurrentGitBranch, isGitRepository } from "../utils/git.ts";
import { isFileResource } from "../utils/utils.ts";
import { SyncOptions } from "./conf.ts";
import { TRIGGER_TYPES } from "../types.ts";
export interface SpecificItemsConfig {
variables?: string[];
resources?: string[];
triggers?: string[];
}
// Define all branch-specific file types (computed lazily)
function getBranchSpecificTypes() {
return {
variable: '.variable.yaml',
resource: '.resource.yaml',
// Generate trigger patterns from the list
...Object.fromEntries(
TRIGGER_TYPES.map(t => [`${t}_trigger`, `.${t}_trigger.yaml`])
)
} as const;
}
/**
* Check if a path ends with any trigger type
*/
function isTriggerFile(path: string): boolean {
return TRIGGER_TYPES.some(type => path.endsWith(`.${type}_trigger.yaml`));
}
/**
* Extract the file type suffix from a path
*/
function getFileTypeSuffix(path: string): string | null {
for (const [_, suffix] of Object.entries(getBranchSpecificTypes())) {
if (path.endsWith(suffix)) {
return suffix;
}
}
const resourceFileMatch = path.match(/(\\.resource\\.file\\..+)$/);
if (resourceFileMatch) {
return resourceFileMatch[1];
}
return null;
}
/**
* Build regex pattern for all supported yaml file types
*/
function buildYamlTypePattern(): string {
const basicTypes = ['variable', 'resource'];
const triggerTypes = TRIGGER_TYPES.map(t => `${t}_trigger`);
return `((${basicTypes.join('|')})|(${triggerTypes.join('|')}))`;
}
/**
@@ -39,6 +88,9 @@ export function getSpecificItemsForCurrentBranch(config: SyncOptions): SpecificI
if (commonItems?.resources) {
merged.resources = [...commonItems.resources];
}
if (commonItems?.triggers) {
merged.triggers = [...commonItems.triggers];
}
// Add branch-specific items (extending common items)
if (branchItems?.variables) {
@@ -47,6 +99,9 @@ export function getSpecificItemsForCurrentBranch(config: SyncOptions): SpecificI
if (branchItems?.resources) {
merged.resources = [...(merged.resources || []), ...branchItems.resources];
}
if (branchItems?.triggers) {
merged.triggers = [...(merged.triggers || []), ...branchItems.triggers];
}
return merged;
}
@@ -75,6 +130,21 @@ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig
return specificItems.resources ? matchesPatterns(path, specificItems.resources) : false;
}
// Check for any trigger type
if (isTriggerFile(path)) {
return specificItems.triggers ? matchesPatterns(path, specificItems.triggers) : false;
}
// Check for resource files using the standard detection function
if (isFileResource(path)) {
// Extract the base path without the file extension to match against patterns
const basePathMatch = path.match(/^(.+?)\.resource\.file\./);
if (basePathMatch && specificItems.resources) {
const basePath = basePathMatch[1] + '.resource.yaml';
return matchesPatterns(basePath, specificItems.resources);
}
}
return false;
}
@@ -82,14 +152,24 @@ export function isSpecificItem(path: string, specificItems: SpecificItemsConfig
* Convert a base path to a branch-specific path
*/
export function toBranchSpecificPath(basePath: string, branchName: string): string {
// Extract the extension (e.g., ".variable.yaml" or ".resource.yaml")
const extensionMatch = basePath.match(/(\.(variable|resource)\.yaml)$/);
if (!extensionMatch) {
return basePath; // Return unchanged if no recognized extension
}
// Check for resource file pattern (e.g., .resource.file.ini)
const resourceFileMatch = basePath.match(/^(.+?)(\.resource\.file\..+)$/);
const extension = extensionMatch[1];
const pathWithoutExtension = basePath.substring(0, basePath.length - extension.length);
let extension: string;
let pathWithoutExtension: string;
if (resourceFileMatch) {
// Handle resource files
extension = resourceFileMatch[2];
pathWithoutExtension = resourceFileMatch[1];
} else {
const suffix = getFileTypeSuffix(basePath);
if (!suffix) {
return basePath;
}
extension = suffix;
pathWithoutExtension = basePath.substring(0, basePath.length - extension.length);
}
// Sanitize branch name to be filesystem-safe
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
@@ -108,17 +188,29 @@ export function toBranchSpecificPath(basePath: string, branchName: string): stri
export function fromBranchSpecificPath(branchSpecificPath: string, branchName: string): string {
// Sanitize branch name the same way as in toBranchSpecificPath
const sanitizedBranchName = branchName.replace(/[\/\\:*?"<>|.]/g, '_');
// Pattern: path.sanitizedBranchName.extension
const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(`\\.${escapedBranchName}(\\.(variable|resource)\\.yaml)$`);
const match = branchSpecificPath.match(pattern);
if (!match) {
// Check for resource file pattern first
const resourceFilePattern = new RegExp(`\\.${escapedBranchName}(\\.resource\\.file\\..+)$`);
const resourceFileMatch = branchSpecificPath.match(resourceFilePattern);
if (resourceFileMatch) {
const extension = resourceFileMatch[1];
const pathWithoutBranchAndExtension = branchSpecificPath.substring(
0,
branchSpecificPath.length - `.${sanitizedBranchName}${extension}`.length
);
return `${pathWithoutBranchAndExtension}${extension}`;
}
const yamlPattern = new RegExp(`\\.${escapedBranchName}(\\.${buildYamlTypePattern()}\\.yaml)$`);
const yamlMatch = branchSpecificPath.match(yamlPattern);
if (!yamlMatch) {
return branchSpecificPath; // Return unchanged if not a branch-specific path
}
const extension = match[1];
const extension = yamlMatch[1];
const pathWithoutBranchAndExtension = branchSpecificPath.substring(
0,
branchSpecificPath.length - `.${sanitizedBranchName}${extension}`.length
@@ -166,10 +258,14 @@ export function isCurrentBranchFile(path: string): boolean {
return false;
}
// Sanitize branch name to match what would be used in file naming
const sanitizedBranchName = currentBranch.replace(/[\/\\:*?"<>|.]/g, '_');
const escapedBranchName = sanitizedBranchName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Use cached pattern or create and cache new one
let pattern = branchPatternCache.get(currentBranch);
if (!pattern) {
pattern = new RegExp(`\\.${currentBranch}\\.(variable|resource)\\.yaml$`);
pattern = new RegExp(`\\.${escapedBranchName}\\.${buildYamlTypePattern()}\\.yaml$|\\.${escapedBranchName}\\.resource\\.file\\..+$`);
branchPatternCache.set(currentBranch, pattern);
}
@@ -181,6 +277,6 @@ export function isCurrentBranchFile(path: string): boolean {
* Used to identify and skip files from other branches during sync operations
*/
export function isBranchSpecificFile(path: string): boolean {
// Pattern: *.branchName.variable.yaml or *.branchName.resource.yaml
return /\.[^.]+\.(variable|resource)\.yaml$/.test(path);
const yamlTypePattern = buildYamlTypePattern();
return new RegExp(`\\.[^.]+\\.${yamlTypePattern}\\.yaml$|\\.[^.]+\\.resource\\.file\\..+$`).test(path);
}
+1 -1
View File
@@ -68,7 +68,7 @@ export {
// }
// });
export const VERSION = "1.535.0";
export const VERSION = "1.538.0";
const command = new Command()
.name("wmill")
+25 -2
View File
@@ -45,6 +45,17 @@ export interface DifferenceChange {
export type Difference = DifferenceCreate | DifferenceRemove | DifferenceChange;
export const TRIGGER_TYPES = [
'http',
'websocket',
'kafka',
'nats',
'postgres',
'mqtt',
'sqs',
'gcp'
] as const;
export type GlobalOptions = {
baseUrl: string | undefined;
workspace: string | undefined;
@@ -111,6 +122,17 @@ export function showConflict(path: string, local: string, remote: string) {
log.info("\n");
}
/**
* Pushes an object to the workspace server based on its type
* @param workspace - The workspace ID to push to
* @param p - The server path (base path for branch-specific items)
* @param befObj - The previous object state (for updates)
* @param newObj - The new object state to push
* @param plainSecrets - Whether to store secrets in plain text
* @param alreadySynced - Array to track already synced items
* @param message - Optional commit/update message
* @param originalLocalPath - The original local file path (used for branch-specific resource file resolution)
*/
export async function pushObj(
workspace: string,
p: string,
@@ -118,7 +140,8 @@ export async function pushObj(
newObj: any,
plainSecrets: boolean,
alreadySynced: string[],
message?: string
message?: string,
originalLocalPath?: string
) {
const typeEnding = getTypeStrFromPath(p);
@@ -135,7 +158,7 @@ export async function pushObj(
} else if (typeEnding === "resource") {
if (!alreadySynced.includes(p)) {
alreadySynced.push(p);
await pushResource(workspace, p, befObj, newObj);
await pushResource(workspace, p, befObj, newObj, originalLocalPath || p);
}
} else if (typeEnding === "resource-type") {
await pushResourceType(workspace, p, befObj, newObj);
+4 -2
View File
@@ -136,10 +136,12 @@ export function sleep(ms: number) {
export function isFileResource(path: string): boolean {
const splitPath = path.split(".");
// Check for pattern: *.resource.file.* (handles both base and branch-specific)
return (
splitPath.length >= 4 &&
splitPath[1] == "resource" &&
splitPath[2] == "file"
splitPath[splitPath.length - 3] == "resource" &&
splitPath[splitPath.length - 2] == "file"
);
}
+34 -163
View File
@@ -1,15 +1,16 @@
{
"name": "windmill-components",
"version": "1.535.0",
"version": "1.538.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.535.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",
+6 -5
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.535.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"
}
}
}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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>
+25 -18
View File
@@ -937,11 +937,21 @@
selected={oneOfSelected}
on:selected={({ detail }) => {
oneOfSelected = detail
const prevValueKeys = Object.keys(
const selectedObjProperties =
oneOf?.find((o) => o.title == detail)?.properties ?? {}
)
const newValueKeys = Object.keys(selectedObjProperties)
const toKeep = {}
for (const key of prevValueKeys) {
for (const key of newValueKeys) {
// Check if there is a select (enum) in the newly selected oneOf and if the current value is not in the enum, skip it
if (
!['kind', 'label'].includes(key) &&
selectedObjProperties[key]?.enum &&
value &&
value[key] !== undefined &&
!selectedObjProperties[key].enum.includes(value[key])
) {
continue
}
toKeep[key] = value[key]
}
const tagKey = oneOf.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
@@ -982,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
@@ -1076,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
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte'
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
@@ -32,8 +33,12 @@
}
}
let enabled = $derived(value != undefined)
$effect(() => {
changeDomain(value?.['domain'], value?.['custom'])
let lastValues = { domain: undefined, custom: undefined }
$effect.pre(() => {
if (value?.['domain'] != lastValues.domain || value?.['custom'] != lastValues.custom) {
lastValues = { domain: value?.['domain'], custom: value?.['custom'] }
untrack(() => changeDomain(value?.['domain'], value?.['custom']))
}
})
</script>
@@ -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">
+18 -14
View File
@@ -32,6 +32,7 @@
defaultModified?: string
readOnly?: boolean
buttons?: ButtonProp[]
modifiedModel?: meditor.ITextModel
}
let {
@@ -44,7 +45,8 @@
defaultOriginal = undefined,
defaultModified = undefined,
readOnly = false,
buttons = []
buttons = [],
modifiedModel
}: Props = $props()
let diffEditor: meditor.IStandaloneDiffEditor | undefined = $state(undefined)
@@ -72,11 +74,8 @@
lineNumbersMinChars: 2,
scrollbar: { alwaysConsumeMouseWheel: false }
})
if (
defaultOriginal !== undefined &&
defaultModified !== undefined &&
defaultLang !== undefined
) {
if (defaultLang !== undefined) {
setupModel(defaultLang, defaultOriginal, defaultModified, defaultModifiedLang)
}
}
@@ -87,16 +86,12 @@
modified?: string,
modifiedLang?: string
) {
const o = meditor.createModel(original ?? '', lang)
const m = modifiedModel ?? meditor.createModel(modified ?? '', modifiedLang ?? lang)
diffEditor?.setModel({
original: meditor.createModel('', lang),
modified: meditor.createModel('', modifiedLang ?? lang)
original: o,
modified: m
})
if (original) {
setOriginal(original)
}
if (modified) {
setModified(modified)
}
}
export function setOriginal(code: string) {
@@ -113,6 +108,15 @@
defaultModified = code
}
export function setModifiedModel(model: meditor.ITextModel) {
const curr = diffEditor?.getModel()
if (!curr) return
diffEditor?.setModel({
original: curr.original,
modified: model
})
}
export function getModified(): string {
return diffEditor?.getModel()?.modified.getValue() ?? ''
}
@@ -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
+96 -11
View File
@@ -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)
@@ -676,7 +726,21 @@
let selectedCode = $state('')
export function reviewAndApplyCode(code: string, applyAll: boolean = false) {
aiChatEditorHandler?.reviewAndApply(code, applyAll)
aiChatEditorHandler?.reviewChanges(code, { applyAll, mode: 'apply' })
}
export function reviewAppliedCode(
originalCode: string,
opts?: { onFinishedReview?: () => void }
) {
aiChatEditorHandler?.reviewChanges(originalCode, {
mode: 'revert',
onFinishedReview: opts?.onFinishedReview
})
}
export function getAiChatEditorHandler() {
return aiChatEditorHandler
}
function addChatHandler(editor: meditor.IStandaloneCodeEditor) {
@@ -1556,6 +1620,7 @@
sqlSchemaCompletor && sqlSchemaCompletor.dispose()
autocompletor && autocompletor.dispose()
sqlTypeCompletor && sqlTypeCompletor.dispose()
preprocessorCompletor && preprocessorCompletor.dispose()
timeoutModel && clearTimeout(timeoutModel)
loadTimeout && clearTimeout(loadTimeout)
aiChatEditorHandler?.clear()
@@ -1607,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)
@@ -1684,10 +1759,20 @@
{#if $reviewingChanges}
<GlobalReviewButtons
onAcceptAll={() => {
aiChatEditorHandler?.acceptAll()
const mode = aiChatEditorHandler?.getReviewMode?.()
if (mode === 'revert') {
aiChatEditorHandler?.keepAll()
} else {
aiChatEditorHandler?.acceptAll()
}
}}
onRejectAll={() => {
aiChatEditorHandler?.rejectAll()
const mode = aiChatEditorHandler?.getReviewMode?.()
if (mode === 'revert') {
aiChatEditorHandler?.revertAll()
} else {
aiChatEditorHandler?.rejectAll()
}
}}
/>
{/if}
@@ -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">
+1 -1
View File
@@ -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,5 @@
<script lang="ts">
import { untrack } from 'svelte'
import CollapseLink from './CollapseLink.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
@@ -12,6 +13,8 @@
let { value = $bindable() }: Props = $props()
let lastValues = { domain: undefined, custom: undefined }
function changeDomain(domain, custom) {
if (value) {
let baseUrl = custom ? `https://${domain}` : `https://${domain}.okta.com`
@@ -32,8 +35,11 @@
}
}
let enabled = $derived(value != undefined)
$effect(() => {
changeDomain(value?.['domain'], value?.['custom'])
$effect.pre(() => {
if (value?.['domain'] != lastValues.domain || value?.['custom'] != lastValues.custom) {
lastValues = { domain: value?.['domain'], custom: value?.['custom'] }
untrack(() => changeDomain(value?.['domain'], value?.['custom']))
}
})
</script>
+11 -7
View File
@@ -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>
+108 -102
View File
@@ -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}
@@ -48,6 +48,7 @@
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import AssetsDropdownButton from './assets/AssetsDropdownButton.svelte'
import { assetEq, type AssetWithAltAccessType } from './assets/lib'
import { editor as meditor } from 'monaco-editor'
interface Props {
// Exported
@@ -80,6 +81,7 @@
disableAi?: boolean
assets?: AssetWithAltAccessType[]
editor_bar_right?: import('svelte').Snippet
enablePreprocessorSnippet?: boolean
}
let {
@@ -110,7 +112,8 @@
lastDeployedCode = undefined,
disableAi = false,
assets = $bindable(),
editor_bar_right
editor_bar_right,
enablePreprocessorSnippet = false
}: Props = $props()
$effect.pre(() => {
@@ -403,7 +406,7 @@
function showDiffMode() {
diffMode = true
diffEditor?.setOriginal(lastDeployedCode ?? '')
diffEditor?.setModified(editor?.getCode() ?? '')
diffEditor?.setModifiedModel(editor?.getModel() as meditor.ITextModel)
diffEditor?.show()
editor?.hide()
}
@@ -624,10 +627,12 @@
automaticLayout={true}
{fixedOverflowWidgets}
{args}
{enablePreprocessorSnippet}
/>
<DiffEditor
className="h-full"
bind:this={diffEditor}
modifiedModel={editor?.getModel() as meditor.ITextModel}
automaticLayout
defaultLang={scriptLangToEditorLang(lang)}
{fixedOverflowWidgets}
+1 -1
View File
@@ -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()
+3 -1
View File
@@ -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
})}

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