mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-18 00:02:27 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f40a3b420b | ||
|
|
c40ad129bc | ||
|
|
7859bca6ae | ||
|
|
1ac391a795 | ||
|
|
5d79f33590 | ||
|
|
86065aaac8 | ||
|
|
e3f4130c68 | ||
|
|
2e582b1bc1 | ||
|
|
2d583826dc | ||
|
|
972ae7aa29 | ||
|
|
d46913b74a | ||
|
|
90f4c64ee1 | ||
|
|
a8cbe9396f | ||
|
|
ce041e8a5e | ||
|
|
65082159d8 | ||
|
|
993fbcde59 | ||
|
|
b1142421b8 |
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## [1.651.1](https://github.com/windmill-labs/windmill/compare/v1.651.0...v1.651.1) (2026-03-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent slow loading toast interval from leaking on promise cancellation ([#8240](https://github.com/windmill-labs/windmill/issues/8240)) ([2e582b1](https://github.com/windmill-labs/windmill/commit/2e582b1bc1c299388a3c97cfddff9d0eb92858f2))
|
||||
* suppress unused variable warnings on windows builds ([#8241](https://github.com/windmill-labs/windmill/issues/8241)) ([2d58382](https://github.com/windmill-labs/windmill/commit/2d583826dc065c05684d4cd1d1510f0d1f2d9ae9))
|
||||
|
||||
## [1.651.0](https://github.com/windmill-labs/windmill/compare/v1.650.0...v1.651.0) (2026-03-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add sandbox annotations, volume mounts, for AI sandbox starting with claude ([#8058](https://github.com/windmill-labs/windmill/issues/8058)) ([5f0ef93](https://github.com/windmill-labs/windmill/commit/5f0ef936d1d5d07d01c8e07e26ec254feebef8fb))
|
||||
* hash-based MCP tool names for long paths ([#8133](https://github.com/windmill-labs/windmill/issues/8133)) ([ce041e8](https://github.com/windmill-labs/windmill/commit/ce041e8a5e7ff105df389875d9981f3843d4ce39))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **python-client:** add delete_s3_object ([#8216](https://github.com/windmill-labs/windmill/issues/8216)) ([90f4c64](https://github.com/windmill-labs/windmill/commit/90f4c64ee12e1d04ce846ff88d6658f667e194e0))
|
||||
* update CLI bun template to match UI template ([#8238](https://github.com/windmill-labs/windmill/issues/8238)) ([a8cbe93](https://github.com/windmill-labs/windmill/commit/a8cbe9396ffc51140dce5582d57f4dc59873304e))
|
||||
* write fallback package.json for codebase mode nsjail ([#8239](https://github.com/windmill-labs/windmill/issues/8239)) ([d46913b](https://github.com/windmill-labs/windmill/commit/d46913b74a0ffd41d2323e0355cc81954f09e29d))
|
||||
|
||||
## [1.650.0](https://github.com/windmill-labs/windmill/compare/v1.649.0...v1.650.0) (2026-03-05)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06"
|
||||
|
||||
Generated
+72
-71
@@ -15741,7 +15741,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15773,6 +15773,7 @@ dependencies = [
|
||||
"sql-builder",
|
||||
"sqlx",
|
||||
"strum 0.27.2",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tikv-jemalloc-ctl",
|
||||
"tikv-jemalloc-sys",
|
||||
@@ -15807,7 +15808,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15820,7 +15821,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15959,7 +15960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15982,7 +15983,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -15995,7 +15996,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16021,7 +16022,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -16031,7 +16032,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16048,7 +16049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"base64 0.22.1",
|
||||
@@ -16071,7 +16072,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16094,7 +16095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16110,7 +16111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16130,7 +16131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16150,7 +16151,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16164,7 +16165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16191,7 +16192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16216,7 +16217,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"flate2",
|
||||
@@ -16234,7 +16235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16255,7 +16256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16275,7 +16276,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16305,7 +16306,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16332,7 +16333,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16344,7 +16345,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.7.9",
|
||||
@@ -16367,7 +16368,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16381,7 +16382,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"chrono",
|
||||
@@ -16412,7 +16413,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16426,7 +16427,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -16445,7 +16446,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -16544,7 +16545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16563,7 +16564,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16578,7 +16579,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16602,7 +16603,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16619,7 +16620,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16635,7 +16636,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16656,7 +16657,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16687,7 +16688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-oauth2",
|
||||
@@ -16711,7 +16712,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -16745,7 +16746,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16763,7 +16764,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16772,7 +16773,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16784,7 +16785,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16796,7 +16797,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -16808,7 +16809,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16820,7 +16821,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16832,7 +16833,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16843,7 +16844,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16854,7 +16855,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16867,7 +16868,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16891,7 +16892,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16905,7 +16906,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16922,7 +16923,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16937,7 +16938,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16956,7 +16957,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16967,7 +16968,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17004,7 +17005,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -17042,7 +17043,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17053,7 +17054,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17082,7 +17083,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -17105,7 +17106,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17138,7 +17139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17158,7 +17159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17192,7 +17193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17227,7 +17228,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17250,7 +17251,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17274,7 +17275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17298,7 +17299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17333,7 +17334,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17361,7 +17362,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17384,7 +17385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
@@ -17402,7 +17403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17508,7 +17509,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -77,7 +77,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.650.0"
|
||||
version = "1.651.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -257,6 +257,7 @@ axum.workspace = true
|
||||
serde.workspace = true
|
||||
windmill-api-client.workspace = true
|
||||
tempfile.workspace = true
|
||||
tar.workspace = true
|
||||
windmill-parser-ts.workspace = true
|
||||
rumqttc.workspace = true
|
||||
rdkafka.workspace = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
05385738e36e81f5bc51d15c0ca60bba30457c21
|
||||
f9549c813b3dba5324ea9d1edacc8756a6d699bf
|
||||
@@ -1,5 +1,6 @@
|
||||
use sqlx::postgres::Postgres;
|
||||
use sqlx::Pool;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::jobs::{JobPayload, RawCode};
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
use windmill_test_utils::*;
|
||||
@@ -1448,3 +1449,240 @@ export function main() { return { a, b }; }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Codebase Mode Tests
|
||||
// ============================================================================
|
||||
|
||||
/// Create a TAR archive in memory containing a single `main.js` file.
|
||||
fn create_codebase_tar(main_js_content: &str) -> Vec<u8> {
|
||||
let mut builder = tar::Builder::new(Vec::new());
|
||||
let content = main_js_content.as_bytes();
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_path("main.js").unwrap();
|
||||
header.set_size(content.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
builder.append(&header, content).unwrap();
|
||||
builder.into_inner().unwrap()
|
||||
}
|
||||
|
||||
/// Place a TAR codebase at the expected cache path for the given job ID and hash.
|
||||
fn place_codebase_in_cache(job_id: &Uuid, tar_bytes: &[u8], is_esm: bool) {
|
||||
let codebase_id = if is_esm {
|
||||
format!("{}.esm.tar", job_id)
|
||||
} else {
|
||||
format!("{}.tar", job_id)
|
||||
};
|
||||
let bundle_path = format!("script_bundle/test-workspace/{}", codebase_id);
|
||||
let cache_path = format!(
|
||||
"{}/{}.tar",
|
||||
*windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
|
||||
bundle_path,
|
||||
);
|
||||
let parent = std::path::Path::new(&cache_path).parent().unwrap();
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
std::fs::write(&cache_path, tar_bytes).unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_cjs_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
module.exports.main = function() {
|
||||
return "cjs codebase ok";
|
||||
};
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "cjs codebase ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, false);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-43), // PREVIEW_IS_TAR_CODEBASE_HASH
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!("cjs codebase ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_esm_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
export function main() {
|
||||
return "esm codebase ok";
|
||||
}
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "esm codebase ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, true);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-45), // PREVIEW_IS_TAR_ESM_CODEBASE_HASH
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await
|
||||
.json_result()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result, serde_json::json!("esm codebase ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_cjs_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
if std::process::Command::new("nsjail")
|
||||
.arg("--help")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("nsjail not found, skipping test");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
module.exports.main = function() {
|
||||
return "cjs nsjail ok";
|
||||
};
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "cjs nsjail ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, false);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-43),
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::Undefined as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let json = result.json_result().unwrap();
|
||||
assert_eq!(json, serde_json::json!("cjs nsjail ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_esm_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
if std::process::Command::new("nsjail")
|
||||
.arg("--help")
|
||||
.output()
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("nsjail not found, skipping test");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let main_js = r#"
|
||||
export function main() {
|
||||
return "esm nsjail ok";
|
||||
}
|
||||
"#;
|
||||
let inner_content = r#"export function main() { return "esm nsjail ok"; }"#;
|
||||
|
||||
let job_id = Uuid::new_v4();
|
||||
let tar_bytes = create_codebase_tar(main_js);
|
||||
place_codebase_in_cache(&job_id, &tar_bytes, true);
|
||||
|
||||
let job = JobPayload::Code(RawCode {
|
||||
hash: Some(-45),
|
||||
content: inner_content.to_string(),
|
||||
path: None,
|
||||
language: ScriptLang::Bun,
|
||||
lock: None,
|
||||
concurrency_settings: Default::default(),
|
||||
debouncing_settings: Default::default(),
|
||||
cache_ttl: None,
|
||||
cache_ignore_s3_path: None,
|
||||
dedicated_worker: None,
|
||||
});
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let result = RunJob::from(job)
|
||||
.job_id(job_id)
|
||||
.run_until_complete(&db, false, port)
|
||||
.await;
|
||||
|
||||
windmill_worker::JOB_ISOLATION.store(
|
||||
windmill_worker::JobIsolationLevel::Undefined as u8,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
|
||||
let json = result.json_result().unwrap();
|
||||
assert_eq!(json, serde_json::json!("esm nsjail ok"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+7
-17
@@ -9,23 +9,7 @@ export async function main() {
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'',
|
||||
'',
|
||||
'f/leafs/ts', 500001, 'nativets', '');
|
||||
|
||||
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
|
||||
'test-workspace',
|
||||
'test-user',
|
||||
'
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("Go leaf")
|
||||
}',
|
||||
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
|
||||
'',
|
||||
'',
|
||||
'f/leafs/go', 500002, 'go', '');
|
||||
'f/leafs/ts', 500001, 'bun', '');
|
||||
|
||||
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
|
||||
'test-workspace',
|
||||
@@ -52,3 +36,9 @@ function main() {
|
||||
'',
|
||||
'f/leafs/php', 500004, 'php', '');
|
||||
|
||||
-- Link scripts to named workspace dependencies (name: "test")
|
||||
INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id) VALUES
|
||||
('test-workspace', 'f/leafs/ts', 'script', 'dependencies/test.package.json', ''),
|
||||
('test-workspace', 'f/leafs/python', 'script', 'dependencies/test.requirements.in', ''),
|
||||
('test-workspace', 'f/leafs/php', 'script', 'dependencies/test.composer.json', '');
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
mod workspace_dependencies {
|
||||
|
||||
use windmill_test_utils::in_test_worker;
|
||||
use windmill_test_utils::init_client;
|
||||
use windmill_test_utils::listen_for_completed_jobs;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use tokio_stream::StreamExt;
|
||||
use windmill_common::scripts::ScriptLang;
|
||||
use windmill_common::workspace_dependencies::WorkspaceDependencies;
|
||||
use windmill_dep_map::workspace_dependencies::NewWorkspaceDependencies;
|
||||
use windmill_test_utils::in_test_worker;
|
||||
use windmill_test_utils::init_client;
|
||||
use windmill_test_utils::listen_for_completed_jobs;
|
||||
|
||||
mod deps {
|
||||
pub const REQUIREMENTS_IN: &'static str = "tiny==0.1.3";
|
||||
// pub const GO_MOD: &'static str = r##"
|
||||
// module example.com/project
|
||||
|
||||
// go 1.20
|
||||
|
||||
// require github.com/gin-gonic/gin v1.8.1
|
||||
// "##;
|
||||
pub const REQUIREMENTS_IN_V2: &'static str = "tiny==0.2.0";
|
||||
|
||||
pub const PACKAGE_JSON: &'static str = r##"
|
||||
{
|
||||
@@ -25,6 +21,18 @@ mod workspace_dependencies {
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
"##;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const PACKAGE_JSON_V2: &'static str = r##"
|
||||
{
|
||||
"name": "example-project",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"express": "^4.18.0",
|
||||
"axios": "^1.0.0"
|
||||
}
|
||||
}
|
||||
"##;
|
||||
|
||||
pub const COMPOSER_JSON: &'static str = r##"
|
||||
@@ -37,9 +45,510 @@ mod workspace_dependencies {
|
||||
"##;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CRUD Tests
|
||||
// =========================================================================
|
||||
|
||||
/// Test: Create workspace dependencies and verify they are stored correctly.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_create_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let id = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("test-deps".to_owned()),
|
||||
description: Some("Test dependencies".to_owned()),
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(id > 0, "Should return a valid ID");
|
||||
|
||||
// Verify it was stored correctly
|
||||
let stored = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await?;
|
||||
assert_eq!(stored.name, Some("test-deps".to_owned()));
|
||||
assert_eq!(stored.content, deps::REQUIREMENTS_IN);
|
||||
assert_eq!(stored.language, ScriptLang::Python3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test: Create unnamed (default) workspace dependencies.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_create_unnamed_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let id = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Bun,
|
||||
content: deps::PACKAGE_JSON.into(),
|
||||
name: None, // Unnamed = default
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(id > 0, "Should return a valid ID");
|
||||
|
||||
// Verify it was stored correctly
|
||||
let stored = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await?;
|
||||
assert_eq!(stored.name, None);
|
||||
assert_eq!(stored.language, ScriptLang::Bun);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test: List workspace dependencies returns all active entries.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_list_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create multiple workspace dependencies
|
||||
for (lang, content, name) in [
|
||||
(ScriptLang::Python3, deps::REQUIREMENTS_IN, Some("python-deps")),
|
||||
(ScriptLang::Bun, deps::PACKAGE_JSON, Some("bun-deps")),
|
||||
(ScriptLang::Bun, deps::PACKAGE_JSON, None), // Default bun deps
|
||||
] {
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: lang,
|
||||
content: content.into(),
|
||||
name: name.map(|s| s.to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
|
||||
assert_eq!(list.len(), 3, "Should have 3 workspace dependencies");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test: Archive workspace dependencies marks them as archived.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_archive_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create workspace dependencies
|
||||
let _id = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("to-archive".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify it exists
|
||||
let list_before = WorkspaceDependencies::list("test-workspace", &db).await?;
|
||||
assert_eq!(list_before.len(), 1);
|
||||
|
||||
// Archive it
|
||||
WorkspaceDependencies::archive(
|
||||
Some("to-archive".to_owned()),
|
||||
ScriptLang::Python3,
|
||||
"test-workspace",
|
||||
&db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify it's no longer in the active list
|
||||
let list_after = WorkspaceDependencies::list("test-workspace", &db).await?;
|
||||
assert_eq!(list_after.len(), 0, "Archived deps should not appear in list");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test: Delete workspace dependencies permanently removes them.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_delete_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create workspace dependencies
|
||||
let id = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("to-delete".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify it exists
|
||||
assert!(
|
||||
WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
// Delete it
|
||||
WorkspaceDependencies::delete(
|
||||
Some("to-delete".to_owned()),
|
||||
ScriptLang::Python3,
|
||||
"test-workspace",
|
||||
&db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Verify it's gone (should error)
|
||||
let result = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await;
|
||||
assert!(result.is_err(), "Deleted deps should not be retrievable");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Version History Tests
|
||||
// =========================================================================
|
||||
|
||||
/// Test: Creating new version archives the old one.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_versioning_archives_previous(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create first version
|
||||
let id1 = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("versioned".to_owned()),
|
||||
description: Some("Version 1".to_owned()),
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create second version with same name
|
||||
let id2 = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN_V2.into(),
|
||||
name: Some("versioned".to_owned()),
|
||||
description: Some("Version 2".to_owned()),
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_ne!(id1, id2, "Should create a new entry");
|
||||
|
||||
// List should only show the active (latest) version
|
||||
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
|
||||
assert_eq!(list.len(), 1, "Should only have 1 active entry");
|
||||
assert_eq!(list[0].content, deps::REQUIREMENTS_IN_V2);
|
||||
|
||||
// History should show both versions
|
||||
let history = WorkspaceDependencies::get_history(
|
||||
Some("versioned".to_owned()),
|
||||
ScriptLang::Python3,
|
||||
"test-workspace",
|
||||
&db,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(history.len(), 2, "Should have 2 versions in history");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test: Description is inherited from previous version if not provided.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_description_inheritance(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create first version with description
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("inherit-desc".to_owned()),
|
||||
description: Some("Original description".to_owned()),
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create second version without description
|
||||
let id2 = NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN_V2.into(),
|
||||
name: Some("inherit-desc".to_owned()),
|
||||
description: None, // Should inherit
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let stored = WorkspaceDependencies::get(id2, "test-workspace".to_owned(), &db).await?;
|
||||
assert_eq!(
|
||||
stored.description,
|
||||
Some("Original description".to_owned()),
|
||||
"Description should be inherited from previous version"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Workspace Isolation Tests
|
||||
// =========================================================================
|
||||
|
||||
/// Test: Workspace dependencies are isolated between workspaces.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_workspace_isolation(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create another workspace
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace (id, name, owner) VALUES ('other-workspace', 'other', 'test-user')"
|
||||
)
|
||||
.execute(&db)
|
||||
.await?;
|
||||
sqlx::query!("INSERT INTO workspace_settings (workspace_id) VALUES ('other-workspace')")
|
||||
.execute(&db)
|
||||
.await?;
|
||||
|
||||
// Create deps in test-workspace
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("shared-name".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create deps in other-workspace with same name
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "other-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN_V2.into(),
|
||||
name: Some("shared-name".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Each workspace should have exactly 1 entry
|
||||
let list1 = WorkspaceDependencies::list("test-workspace", &db).await?;
|
||||
let list2 = WorkspaceDependencies::list("other-workspace", &db).await?;
|
||||
|
||||
assert_eq!(list1.len(), 1);
|
||||
assert_eq!(list2.len(), 1);
|
||||
|
||||
// Content should be different
|
||||
assert_eq!(list1[0].content, deps::REQUIREMENTS_IN);
|
||||
assert_eq!(list2[0].content, deps::REQUIREMENTS_IN_V2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Language-specific Tests
|
||||
// =========================================================================
|
||||
|
||||
/// Test: Different languages can have same-named workspace dependencies.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_same_name_different_languages(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
// Create Python deps
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Python3,
|
||||
content: deps::REQUIREMENTS_IN.into(),
|
||||
name: Some("common".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create Bun deps with same name
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Bun,
|
||||
content: deps::PACKAGE_JSON.into(),
|
||||
name: Some("common".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
|
||||
assert_eq!(list.len(), 2, "Should have 2 entries (different languages)");
|
||||
|
||||
let python_deps: Vec<_> = list
|
||||
.iter()
|
||||
.filter(|d| d.language == ScriptLang::Python3)
|
||||
.collect();
|
||||
let bun_deps: Vec<_> = list
|
||||
.iter()
|
||||
.filter(|d| d.language == ScriptLang::Bun)
|
||||
.collect();
|
||||
|
||||
assert_eq!(python_deps.len(), 1);
|
||||
assert_eq!(bun_deps.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test: Nativets and Bunnative use Bun workspace dependencies.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_nativets_uses_bun_deps(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
use windmill_common::worker::Connection;
|
||||
|
||||
// Create Bun deps (which Nativets should use)
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: ScriptLang::Bun,
|
||||
content: deps::PACKAGE_JSON.into(),
|
||||
name: None,
|
||||
description: None,
|
||||
}
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test".to_owned(),
|
||||
"test".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Query for Nativets should return Bun deps
|
||||
let result = WorkspaceDependencies::get_latest(
|
||||
None,
|
||||
ScriptLang::Nativets,
|
||||
"test-workspace",
|
||||
Connection::Sql(db.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(result.is_some(), "Nativets should find Bun deps");
|
||||
assert_eq!(result.unwrap().language, ScriptLang::Bun);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Path Generation Tests
|
||||
// =========================================================================
|
||||
|
||||
/// Test: to_path generates correct paths for named and unnamed deps.
|
||||
#[test]
|
||||
fn test_to_path_generation() {
|
||||
// Unnamed (default) deps
|
||||
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Python3).unwrap();
|
||||
assert_eq!(path, "dependencies/requirements.in");
|
||||
|
||||
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Bun).unwrap();
|
||||
assert_eq!(path, "dependencies/package.json");
|
||||
|
||||
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Php).unwrap();
|
||||
assert_eq!(path, "dependencies/composer.json");
|
||||
|
||||
// Named deps
|
||||
let path =
|
||||
WorkspaceDependencies::to_path(&Some("custom".to_owned()), ScriptLang::Python3).unwrap();
|
||||
assert_eq!(path, "dependencies/custom.requirements.in");
|
||||
|
||||
let path =
|
||||
WorkspaceDependencies::to_path(&Some("custom".to_owned()), ScriptLang::Bun).unwrap();
|
||||
assert_eq!(path, "dependencies/custom.package.json");
|
||||
}
|
||||
|
||||
/// Test: to_path returns error for unsupported languages.
|
||||
#[test]
|
||||
fn test_to_path_unsupported_language() {
|
||||
// Deno doesn't support workspace dependencies
|
||||
let result = WorkspaceDependencies::to_path(&None, ScriptLang::Deno);
|
||||
assert!(result.is_err(), "Deno should not support workspace deps");
|
||||
}
|
||||
|
||||
/// Test E2E: Creating named workspace dependencies triggers re-lock jobs for dependent scripts.
|
||||
///
|
||||
/// This test:
|
||||
/// 1. Uses fixture with Python, Bun, PHP scripts linked to named workspace deps via dependency_map
|
||||
/// 2. Creates named workspace dependencies for each language
|
||||
/// 3. Verifies dependency jobs are triggered for all linked scripts
|
||||
#[cfg(feature = "python")]
|
||||
#[sqlx::test(fixtures("base", "workspace_dependencies_leafs"))]
|
||||
#[ignore]
|
||||
async fn basic_manual_named(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
let ((_client, port, _s), db, mut completed) = (
|
||||
init_client(db.clone()).await,
|
||||
@@ -47,67 +556,69 @@ mod workspace_dependencies {
|
||||
listen_for_completed_jobs(&db).await,
|
||||
);
|
||||
|
||||
for (idx, (l, c)) in [
|
||||
// Create named workspace dependencies for Python, Bun, and PHP
|
||||
// These will trigger dependency jobs for scripts linked via dependency_map
|
||||
for (lang, content) in [
|
||||
(ScriptLang::Python3, deps::REQUIREMENTS_IN),
|
||||
(ScriptLang::Bun, deps::PACKAGE_JSON),
|
||||
(ScriptLang::Php, deps::COMPOSER_JSON),
|
||||
// (ScriptLang::Go, deps::GO_MOD),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let id = NewWorkspaceDependencies {
|
||||
] {
|
||||
NewWorkspaceDependencies {
|
||||
workspace_id: "test-workspace".into(),
|
||||
language: *l,
|
||||
content: (*c).into(),
|
||||
language: lang,
|
||||
content: content.into(),
|
||||
name: Some("test".to_owned()),
|
||||
description: None,
|
||||
}
|
||||
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(idx + 1, id as usize);
|
||||
.create(
|
||||
(
|
||||
"test@test.com".to_owned(),
|
||||
"u/test-user".to_owned(),
|
||||
"test-user".to_owned(),
|
||||
),
|
||||
db.clone(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Wait for 4 jobs.
|
||||
// Creating those dependencies will trigger redeployment of all scripts in workspace_dependencies_leafs.sql
|
||||
in_test_worker(
|
||||
db,
|
||||
async {
|
||||
completed.next().await;
|
||||
completed.next().await;
|
||||
completed.next().await;
|
||||
// completed.next().await;
|
||||
},
|
||||
port,
|
||||
)
|
||||
.await;
|
||||
// Wait for 3 dependency jobs (one per script in fixture)
|
||||
let mut completed_paths = vec![];
|
||||
for _ in 0..3 {
|
||||
let job_id = in_test_worker(db, async { completed.next().await }, port)
|
||||
.await
|
||||
.expect("Expected a dependency job to complete");
|
||||
|
||||
// Verify all scripts have correct locks
|
||||
// let mut langs = vec![];
|
||||
// for r in sqlx::query!(
|
||||
// r#"SELECT language AS "language: ScriptLang",lock FROM script WHERE archived = false"#
|
||||
// )
|
||||
// .fetch_all(db)
|
||||
// .await
|
||||
// .unwrap()
|
||||
// {
|
||||
// match r.language {
|
||||
// ScriptLang::Python3 => assert_eq!("", &r.lock.unwrap()),
|
||||
// ScriptLang::Go => todo!(),
|
||||
// ScriptLang::Bun => todo!(),
|
||||
// ScriptLang::Bunnative => todo!(),
|
||||
// ScriptLang::Php => todo!(),
|
||||
// _ => panic!("Unsupported language"),
|
||||
// }
|
||||
let job_path = sqlx::query_scalar!(
|
||||
"SELECT runnable_path FROM v2_job WHERE id = $1",
|
||||
job_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
// langs.push(r.language);
|
||||
// }
|
||||
if let Some(path) = job_path {
|
||||
completed_paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
// langs.sort();
|
||||
// // Just tiny additional verification for peace of mind.
|
||||
// assert_eq!(langs.as_slice(), &[]);
|
||||
// Verify all 3 scripts received dependency jobs
|
||||
completed_paths.sort();
|
||||
let expected = vec![
|
||||
"f/leafs/php".to_string(),
|
||||
"f/leafs/python".to_string(),
|
||||
"f/leafs/ts".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
completed_paths, expected,
|
||||
"All scripts should have received dependency jobs"
|
||||
);
|
||||
|
||||
// Verify no extra jobs were created
|
||||
let total_jobs = sqlx::query_scalar!("SELECT COUNT(*) FROM v2_job")
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
|
||||
assert_eq!(total_jobs, Some(3), "Should have exactly 3 jobs");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.650.0
|
||||
version: 1.651.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -64,9 +64,10 @@ impl McpBackend for WindmillBackend {
|
||||
auth: &ApiAuthed,
|
||||
workspace_id: &str,
|
||||
favorites_only: bool,
|
||||
path_prefix: Option<&str>,
|
||||
) -> BackendResult<Vec<ScriptInfo>> {
|
||||
let scope_type = if favorites_only { "favorites" } else { "all" };
|
||||
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script")
|
||||
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script", path_prefix)
|
||||
.await
|
||||
.map_err(|e| ErrorData::internal_error(e.message, None))
|
||||
}
|
||||
@@ -76,9 +77,10 @@ impl McpBackend for WindmillBackend {
|
||||
auth: &ApiAuthed,
|
||||
workspace_id: &str,
|
||||
favorites_only: bool,
|
||||
path_prefix: Option<&str>,
|
||||
) -> BackendResult<Vec<FlowInfo>> {
|
||||
let scope_type = if favorites_only { "favorites" } else { "all" };
|
||||
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow")
|
||||
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow", path_prefix)
|
||||
.await
|
||||
.map_err(|e| ErrorData::internal_error(e.message, None))
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
|
||||
workspace_id: &str,
|
||||
scope_type: &str,
|
||||
item_type: &str,
|
||||
path_prefix: Option<&str>,
|
||||
) -> Result<Vec<T>, ErrorData> {
|
||||
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
|
||||
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
|
||||
@@ -153,6 +154,11 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
|
||||
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
|
||||
}
|
||||
|
||||
if let Some(prefix) = path_prefix {
|
||||
let escaped = prefix.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
|
||||
sqlb.and_where("o.path LIKE ? ESCAPE '\\'".bind(&format!("{}%", escaped)));
|
||||
}
|
||||
|
||||
sqlb.order_by(
|
||||
if item_type == "flow" {
|
||||
"o.edited_at"
|
||||
|
||||
@@ -11,6 +11,8 @@ pub mod types;
|
||||
pub use schema::convert_schema_to_schema_type;
|
||||
pub use scope::{is_resource_allowed, parse_mcp_scopes, McpScopeConfig};
|
||||
pub use transform::{
|
||||
apply_key_transformation, reverse_transform, reverse_transform_key, transform_path,
|
||||
apply_key_transformation, extract_hub_version_id_from_hashed,
|
||||
extract_path_prefix_from_hashed, parse_tool_prefix, reverse_transform, reverse_transform_key,
|
||||
transform_hub_path, transform_path,
|
||||
};
|
||||
pub use types::*;
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
//! to make them compatible with MCP tool naming requirements.
|
||||
|
||||
use super::types::SchemaType;
|
||||
use windmill_common::utils::calculate_hash;
|
||||
|
||||
/// MCP clients do not allow names longer than 60 characters
|
||||
const MAX_PATH_LENGTH: usize = 60;
|
||||
/// Max tool name length. The MCP spec allows 64 chars, but some clients
|
||||
/// (e.g. Cursor) prepend the server name to the tool name, so we use 40
|
||||
/// to leave room for that prefix.
|
||||
const MAX_PATH_LENGTH: usize = 40;
|
||||
|
||||
/// Length of the SHA256 hash suffix used for hashed names
|
||||
const HASH_LEN: usize = 16;
|
||||
|
||||
/// Transform the path for workspace scripts/flows
|
||||
///
|
||||
@@ -14,19 +20,133 @@ const MAX_PATH_LENGTH: usize = 60;
|
||||
/// path with the type prefix. This is used when listing, because we can't
|
||||
/// have names with slashes. Because we replace slashes with underscores,
|
||||
/// we also need to escape underscores.
|
||||
///
|
||||
/// For short names (≤40 chars): `s-{escaped_path}` or `f-{escaped_path}`
|
||||
/// For long names (>40 chars): `S-{escaped[:22]}{sha256[:16]}` or `F-{escaped[:22]}{sha256[:16]}`
|
||||
///
|
||||
/// The uppercase prefix signals that the name is hashed.
|
||||
pub fn transform_path(path: &str, type_str: &str) -> String {
|
||||
let escaped_path = path.replace('_', "__").replace('/', "_");
|
||||
// first letter of type_str is used as prefix, only one letter to avoid reaching 60 char name limit
|
||||
let transformed_path = format!("{}-{}", &type_str[..1], escaped_path);
|
||||
if transformed_path.len() > MAX_PATH_LENGTH {
|
||||
let suffix = "_TRUNC";
|
||||
return format!(
|
||||
"{}{}",
|
||||
&transformed_path[..MAX_PATH_LENGTH - suffix.len()],
|
||||
suffix
|
||||
);
|
||||
let prefix_char = &type_str[..1];
|
||||
let short_name = format!("{}-{}", prefix_char, escaped_path);
|
||||
|
||||
if short_name.len() <= MAX_PATH_LENGTH {
|
||||
return short_name;
|
||||
}
|
||||
transformed_path
|
||||
|
||||
let upper_prefix = prefix_char.to_uppercase();
|
||||
// Layout: "{Upper}-" (2 chars) + prefix_body (22 chars) + hash (16 chars) = 40
|
||||
let prefix_body_len = MAX_PATH_LENGTH - 2 - HASH_LEN;
|
||||
let hash = calculate_hash(&short_name);
|
||||
let hash_suffix = &hash[..HASH_LEN];
|
||||
let truncated = truncate_to_char_boundary(&escaped_path, prefix_body_len);
|
||||
format!("{}-{}{}", upper_prefix, truncated, hash_suffix)
|
||||
}
|
||||
|
||||
/// Transform the path for hub scripts
|
||||
///
|
||||
/// For short names (≤40 chars): `hs-{id}-{summary}`
|
||||
/// For long names (>40 chars): `Hs-{id}-{summary[:N]}{sha256[:16]}`
|
||||
pub fn transform_hub_path(version_id: u64, summary: &str) -> String {
|
||||
let escaped_summary = summary.replace(' ', "_");
|
||||
let short_name = format!("hs-{}-{}", version_id, escaped_summary);
|
||||
|
||||
if short_name.len() <= MAX_PATH_LENGTH {
|
||||
return short_name;
|
||||
}
|
||||
|
||||
let hash = calculate_hash(&short_name);
|
||||
let hash_suffix = &hash[..HASH_LEN];
|
||||
// "Hs-{id}-" prefix, then fill remaining with summary + hash
|
||||
let fixed_prefix = format!("Hs-{}-", version_id);
|
||||
let available = MAX_PATH_LENGTH - fixed_prefix.len() - HASH_LEN;
|
||||
let truncated_summary = truncate_to_char_boundary(&escaped_summary, available);
|
||||
format!("{}{}{}", fixed_prefix, truncated_summary, hash_suffix)
|
||||
}
|
||||
|
||||
/// Parse the prefix of any tool name (both short and hashed).
|
||||
/// Returns `(type_str, is_hub, is_hashed)`.
|
||||
/// Hashed names use an uppercase first character as the signal.
|
||||
pub fn parse_tool_prefix(name: &str) -> Result<(&str, bool, bool), String> {
|
||||
let is_hashed = name.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false);
|
||||
let lower = name.to_ascii_lowercase();
|
||||
let (type_str, is_hub) = if lower.starts_with("hs-") {
|
||||
("script", true)
|
||||
} else if lower.starts_with("s-") {
|
||||
("script", false)
|
||||
} else if lower.starts_with("f-") {
|
||||
("flow", false)
|
||||
} else {
|
||||
return Err(format!("Invalid tool name prefix: {}", name));
|
||||
};
|
||||
Ok((type_str, is_hub, is_hashed))
|
||||
}
|
||||
|
||||
/// Extract the hub version_id from a hashed hub script name like `Hs-{id}-...`
|
||||
pub fn extract_hub_version_id_from_hashed(name: &str) -> Result<String, String> {
|
||||
let rest = name
|
||||
.strip_prefix("Hs-")
|
||||
.ok_or_else(|| format!("Not a hashed hub name: {}", name))?;
|
||||
let id = rest
|
||||
.split('-')
|
||||
.next()
|
||||
.ok_or_else(|| format!("No version_id in hashed hub name: {}", name))?;
|
||||
if id.is_empty() {
|
||||
return Err(format!("Empty version_id in hashed hub name: {}", name));
|
||||
}
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
/// Extract a safe original-path prefix from a hashed tool name.
|
||||
///
|
||||
/// Given `S-u_admin_engineering__te<hash16>`, extracts the escaped prefix between
|
||||
/// the type prefix (`S-`, `F-`, or `Hs-`) and the hash, un-escapes it, and
|
||||
/// returns a prefix suitable for `WHERE path LIKE '{prefix}%'`.
|
||||
///
|
||||
/// Returns `None` if the name is too short or has an unrecognized prefix.
|
||||
pub fn extract_path_prefix_from_hashed(name: &str) -> Option<String> {
|
||||
let prefix_len = if name.starts_with("Hs-") {
|
||||
3
|
||||
} else if name.starts_with("S-") || name.starts_with("F-") {
|
||||
2
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if name.len() <= prefix_len + HASH_LEN {
|
||||
return None;
|
||||
}
|
||||
let escaped_prefix = &name[prefix_len..name.len() - HASH_LEN];
|
||||
if escaped_prefix.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Strip trailing underscores — they may be half of a `__` pair split by truncation
|
||||
let trimmed = escaped_prefix.trim_end_matches('_');
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(unescape_path(trimmed))
|
||||
}
|
||||
|
||||
/// Un-escape a mangled path segment: `__` → `_`, standalone `_` → `/`.
|
||||
fn unescape_path(s: &str) -> String {
|
||||
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
|
||||
s.replace("__", TEMP_PLACEHOLDER)
|
||||
.replace('_', "/")
|
||||
.replace(TEMP_PLACEHOLDER, "_")
|
||||
}
|
||||
|
||||
/// Truncate a string to at most `max_len` bytes, ensuring we don't split a UTF-8 character.
|
||||
fn truncate_to_char_boundary(s: &str, max_len: usize) -> &str {
|
||||
if s.len() <= max_len {
|
||||
return s;
|
||||
}
|
||||
let mut end = max_len;
|
||||
while end > 0 && !s.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
&s[..end]
|
||||
}
|
||||
|
||||
/// Reverse the transformation of a path
|
||||
@@ -38,25 +158,22 @@ pub fn transform_path(path: &str, type_str: &str) -> String {
|
||||
/// This is used in call_tool to get the original path, and the type of the item.
|
||||
///
|
||||
/// Returns: (type, original_path, is_hub)
|
||||
///
|
||||
/// Note: This only works for non-hashed (short) names. Hashed names must be
|
||||
/// resolved via `parse_tool_prefix` + path enumeration in the runner.
|
||||
pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool), String> {
|
||||
let is_hub = transformed_path.starts_with("h");
|
||||
let transformed_path = if is_hub {
|
||||
transformed_path[1..].to_string()
|
||||
} else {
|
||||
transformed_path.to_string()
|
||||
};
|
||||
let type_str = if transformed_path.starts_with("s-") {
|
||||
"script"
|
||||
} else if transformed_path.starts_with("f-") {
|
||||
"flow"
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Invalid prefix in transformed path: {}",
|
||||
transformed_path
|
||||
));
|
||||
};
|
||||
let (type_str, is_hub, is_hashed) = parse_tool_prefix(transformed_path)?;
|
||||
|
||||
let mangled_path = &transformed_path[2..];
|
||||
if is_hashed {
|
||||
return Err(
|
||||
"Hashed names cannot be reverse-transformed directly; use path enumeration instead"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Strip the prefix: "hs-" (3 chars) for hub, "s-"/"f-" (2 chars) for others
|
||||
let prefix_len = if is_hub { 3 } else { 2 };
|
||||
let mangled_path = &transformed_path[prefix_len..];
|
||||
|
||||
let original_path = if is_hub {
|
||||
let parts = mangled_path.split("-").collect::<Vec<&str>>();
|
||||
@@ -65,11 +182,7 @@ pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool),
|
||||
}
|
||||
parts[0].to_string()
|
||||
} else {
|
||||
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
|
||||
mangled_path
|
||||
.replace("__", TEMP_PLACEHOLDER)
|
||||
.replace('_', "/")
|
||||
.replace(TEMP_PLACEHOLDER, "_")
|
||||
unescape_path(mangled_path)
|
||||
};
|
||||
|
||||
Ok((type_str, original_path, is_hub))
|
||||
@@ -97,16 +210,13 @@ pub fn reverse_transform_key(transformed_key: &str, schema_obj: &Option<SchemaTy
|
||||
let schema_obj = match schema_obj {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
// No schema available, return the key as is (best guess)
|
||||
return transformed_key.to_string();
|
||||
}
|
||||
};
|
||||
|
||||
for original_key_in_schema in schema_obj.properties.keys() {
|
||||
// Apply the SAME forward transformation to the schema key
|
||||
let potential_transformed_key = apply_key_transformation(original_key_in_schema);
|
||||
|
||||
// If it matches the key we received, we found the likely original
|
||||
if potential_transformed_key == transformed_key {
|
||||
return original_key_in_schema.clone();
|
||||
}
|
||||
@@ -120,7 +230,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_transform_path() {
|
||||
fn test_transform_path_short() {
|
||||
assert_eq!(
|
||||
transform_path("u/admin/script", "script"),
|
||||
"s-u_admin_script"
|
||||
@@ -130,7 +240,108 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_transform() {
|
||||
fn test_transform_path_long_is_hashed() {
|
||||
let long_path = "u/engineering/team/automation/very_long_script_name_that_exceeds_limit";
|
||||
let result = transform_path(long_path, "script");
|
||||
assert_eq!(result.len(), MAX_PATH_LENGTH);
|
||||
assert!(result.starts_with("S-"));
|
||||
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
|
||||
assert!(is_hashed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_path_long_flow_is_hashed() {
|
||||
let long_path = "f/engineering/team/automation/very_long_flow_name_that_exceeds_limit";
|
||||
let result = transform_path(long_path, "flow");
|
||||
assert_eq!(result.len(), MAX_PATH_LENGTH);
|
||||
assert!(result.starts_with("F-"));
|
||||
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
|
||||
assert!(is_hashed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_path_hashing_is_deterministic() {
|
||||
let path = "u/engineering/team/automation/very_long_script_name_that_exceeds_limit";
|
||||
let a = transform_path(path, "script");
|
||||
let b = transform_path(path, "script");
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_path_different_long_paths_differ() {
|
||||
let a = transform_path(
|
||||
"u/engineering/team/automation/very_long_script_name_that_exceeds_limit_a",
|
||||
"script",
|
||||
);
|
||||
let b = transform_path(
|
||||
"u/engineering/team/automation/very_long_script_name_that_exceeds_limit_b",
|
||||
"script",
|
||||
);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_hub_path_short() {
|
||||
let result = transform_hub_path(12345, "Send Slack Message");
|
||||
assert_eq!(result, "hs-12345-Send_Slack_Message");
|
||||
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
|
||||
assert!(!is_hashed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_hub_path_long_is_hashed() {
|
||||
let result = transform_hub_path(
|
||||
12345,
|
||||
"Send Slack Message To Channel With Very Long Description That Exceeds Limit",
|
||||
);
|
||||
assert_eq!(result.len(), MAX_PATH_LENGTH);
|
||||
assert!(result.starts_with("Hs-12345-"));
|
||||
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
|
||||
assert!(is_hashed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_hub_version_id_from_hashed() {
|
||||
let name = "Hs-12345-Send_Slack_Message_To_Ch9e8d7c6b5a4f3e2d";
|
||||
let id = extract_hub_version_id_from_hashed(name).unwrap();
|
||||
assert_eq!(id, "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_tool_prefix() {
|
||||
let (t, hub, hashed) = parse_tool_prefix("S-something").unwrap();
|
||||
assert_eq!(t, "script");
|
||||
assert!(!hub);
|
||||
assert!(hashed);
|
||||
|
||||
let (t, hub, hashed) = parse_tool_prefix("F-something").unwrap();
|
||||
assert_eq!(t, "flow");
|
||||
assert!(!hub);
|
||||
assert!(hashed);
|
||||
|
||||
let (t, hub, hashed) = parse_tool_prefix("Hs-12345-something").unwrap();
|
||||
assert_eq!(t, "script");
|
||||
assert!(hub);
|
||||
assert!(hashed);
|
||||
|
||||
let (t, hub, hashed) = parse_tool_prefix("s-u_admin_script").unwrap();
|
||||
assert_eq!(t, "script");
|
||||
assert!(!hub);
|
||||
assert!(!hashed);
|
||||
|
||||
let (t, hub, hashed) = parse_tool_prefix("f-f_folder_flow").unwrap();
|
||||
assert_eq!(t, "flow");
|
||||
assert!(!hub);
|
||||
assert!(!hashed);
|
||||
|
||||
let (t, hub, hashed) = parse_tool_prefix("hs-12345-summary").unwrap();
|
||||
assert_eq!(t, "script");
|
||||
assert!(hub);
|
||||
assert!(!hashed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_transform_short_names() {
|
||||
let (type_str, path, is_hub) = reverse_transform("s-u_admin_script").unwrap();
|
||||
assert_eq!(type_str, "script");
|
||||
assert_eq!(path, "u/admin/script");
|
||||
@@ -142,6 +353,70 @@ mod tests {
|
||||
assert!(!is_hub);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_path_prefix_from_hashed() {
|
||||
// Generate a real hashed name and verify prefix extraction
|
||||
let long_path = "u/admin/engineering/team/automation/very_long_script";
|
||||
let hashed = transform_path(long_path, "script");
|
||||
let (_, _, is_hashed) = parse_tool_prefix(&hashed).unwrap();
|
||||
assert!(is_hashed);
|
||||
|
||||
let prefix = extract_path_prefix_from_hashed(&hashed).unwrap();
|
||||
// The original path should start with the extracted prefix
|
||||
assert!(
|
||||
long_path.starts_with(&prefix),
|
||||
"path '{}' should start with prefix '{}'",
|
||||
long_path,
|
||||
prefix
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_path_prefix_underscore_in_path() {
|
||||
let long_path = "u/admin/my_team/automation/very_long_script_name_here";
|
||||
let hashed = transform_path(long_path, "script");
|
||||
let prefix = extract_path_prefix_from_hashed(&hashed).unwrap();
|
||||
assert!(
|
||||
long_path.starts_with(&prefix),
|
||||
"path '{}' should start with prefix '{}'",
|
||||
long_path,
|
||||
prefix
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_path_prefix_rejects_invalid_prefix() {
|
||||
assert!(extract_path_prefix_from_hashed("x-something").is_none());
|
||||
assert!(extract_path_prefix_from_hashed("").is_none());
|
||||
assert!(extract_path_prefix_from_hashed("S-").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_path_prefix_handles_hs_prefix() {
|
||||
// Hs- is 3 chars, not 2 — ensure the prefix is stripped correctly
|
||||
let hashed = transform_hub_path(12345, "a]very long hub script summary that exceeds the limit");
|
||||
let (_, is_hub, is_hashed) = parse_tool_prefix(&hashed).unwrap();
|
||||
assert!(is_hub);
|
||||
assert!(is_hashed);
|
||||
|
||||
let prefix = extract_path_prefix_from_hashed(&hashed);
|
||||
// Should not start with 's' (leftover from Hs- if sliced at index 2)
|
||||
if let Some(ref p) = prefix {
|
||||
assert!(
|
||||
!p.starts_with('s'),
|
||||
"prefix '{}' should not start with 's' from mis-sliced Hs- prefix",
|
||||
p
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_transform_rejects_hashed_names() {
|
||||
assert!(reverse_transform("S-something").is_err());
|
||||
assert!(reverse_transform("F-something").is_err());
|
||||
assert!(reverse_transform("Hs-12345-something").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_key_transformation() {
|
||||
assert_eq!(apply_key_transformation("my key"), "my_key");
|
||||
|
||||
@@ -92,8 +92,10 @@ pub struct ItemSchema {
|
||||
|
||||
/// Trait for objects that can be converted to MCP tools
|
||||
pub trait ToolableItem {
|
||||
/// Get the path or identifier for this item (transformed for MCP compatibility)
|
||||
fn get_path_or_id(&self) -> String;
|
||||
/// Get the MCP-compatible tool name (path transformed with escaping/hashing)
|
||||
fn get_transformed_path(&self) -> String;
|
||||
/// Get the original full path of this item (for display in tool title)
|
||||
fn get_full_path(&self) -> &str;
|
||||
/// Get the summary/title of this item
|
||||
fn get_summary(&self) -> &str;
|
||||
/// Get the description of this item
|
||||
|
||||
@@ -14,9 +14,9 @@ pub mod client;
|
||||
|
||||
// Re-export common types at crate root for convenience
|
||||
pub use common::{
|
||||
convert_schema_to_schema_type, is_resource_allowed, parse_mcp_scopes, transform_path, FlowInfo,
|
||||
HubResponse, HubScriptInfo, ItemSchema, McpScopeConfig, ResourceInfo, ResourceType, SchemaType,
|
||||
ScriptInfo, ToolableItem, WorkspaceId,
|
||||
convert_schema_to_schema_type, is_resource_allowed, parse_mcp_scopes, transform_hub_path,
|
||||
transform_path, FlowInfo, HubResponse, HubScriptInfo, ItemSchema, McpScopeConfig, ResourceInfo,
|
||||
ResourceType, SchemaType, ScriptInfo, ToolableItem, WorkspaceId,
|
||||
};
|
||||
|
||||
// Re-export client types at crate root for backward compatibility
|
||||
|
||||
@@ -55,20 +55,22 @@ pub trait McpBackend: Send + Sync + Clone + 'static {
|
||||
// Listing Operations
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// List scripts, optionally filtered to favorites only
|
||||
/// List scripts, optionally filtered to favorites only and/or by path prefix
|
||||
async fn list_scripts(
|
||||
&self,
|
||||
auth: &Self::Auth,
|
||||
workspace_id: &str,
|
||||
favorites_only: bool,
|
||||
path_prefix: Option<&str>,
|
||||
) -> BackendResult<Vec<ScriptInfo>>;
|
||||
|
||||
/// List flows, optionally filtered to favorites only
|
||||
/// List flows, optionally filtered to favorites only and/or by path prefix
|
||||
async fn list_flows(
|
||||
&self,
|
||||
auth: &Self::Auth,
|
||||
workspace_id: &str,
|
||||
favorites_only: bool,
|
||||
path_prefix: Option<&str>,
|
||||
) -> BackendResult<Vec<FlowInfo>>;
|
||||
|
||||
/// List resource types in workspace
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
use crate::common::schema::extract_resource_types_from_schema;
|
||||
use crate::common::scope::parse_mcp_scopes;
|
||||
use crate::common::transform::{reverse_transform, reverse_transform_key};
|
||||
use crate::common::transform::{
|
||||
extract_hub_version_id_from_hashed, extract_path_prefix_from_hashed, parse_tool_prefix,
|
||||
reverse_transform, reverse_transform_key,
|
||||
};
|
||||
use crate::common::types::{ResourceInfo, ToolableItem, WorkspaceId};
|
||||
use crate::server::backend::{McpAuth, McpBackend};
|
||||
use crate::server::endpoints::endpoint_tool_to_mcp_tool;
|
||||
@@ -81,6 +84,13 @@ impl<B: McpBackend> Runner<B> {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_matching_path<T: ToolableItem>(candidates: Vec<T>, request_name: &str) -> Option<String> {
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|item| item.get_transformed_path() == request_name)
|
||||
.map(|item| item.get_full_path().to_string())
|
||||
}
|
||||
|
||||
impl<B: McpBackend> ServerHandler for Runner<B> {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
@@ -120,9 +130,9 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
|
||||
// Fetch all items concurrently
|
||||
let (scripts, flows, resource_types, hub_scripts) = tokio::try_join!(
|
||||
self.backend
|
||||
.list_scripts(&auth, &workspace_id, favorites_only),
|
||||
.list_scripts(&auth, &workspace_id, favorites_only, None),
|
||||
self.backend
|
||||
.list_flows(&auth, &workspace_id, favorites_only),
|
||||
.list_flows(&auth, &workspace_id, favorites_only, None),
|
||||
self.backend.list_resource_types(&auth, &workspace_id),
|
||||
async {
|
||||
if let Some(ref apps) = scope_config.hub_apps {
|
||||
@@ -231,17 +241,6 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
|
||||
let scope_config =
|
||||
parse_mcp_scopes(scopes).map_err(|e| ErrorData::internal_error(e, None))?;
|
||||
|
||||
// Handle truncated tool names
|
||||
if request.name.ends_with("_TRUNC") {
|
||||
return Ok(CallToolResult::error(vec![rmcp::model::Annotated::new(
|
||||
rmcp::model::RawContent::Text(rmcp::model::RawTextContent {
|
||||
text: "Tool path is too long. Consider shortening it to make it compatible with MCP.".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
None,
|
||||
)]));
|
||||
}
|
||||
|
||||
let args = request.arguments.map(Value::Object).unwrap_or(Value::Null);
|
||||
|
||||
// Check if this is an endpoint tool
|
||||
@@ -274,10 +273,58 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
|
||||
}
|
||||
}
|
||||
|
||||
// Not an endpoint tool - parse as script/flow
|
||||
let (tool_type, path, is_hub) = reverse_transform(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
|
||||
})?;
|
||||
// Resolve the tool name to (type, path, is_hub)
|
||||
let (type_str, is_hub, is_hashed) =
|
||||
parse_tool_prefix(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
|
||||
})?;
|
||||
|
||||
let (tool_type, path, is_hub) = if !is_hashed {
|
||||
reverse_transform(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
|
||||
})?
|
||||
} else if is_hub {
|
||||
let version_id =
|
||||
extract_hub_version_id_from_hashed(&request.name).map_err(|e| {
|
||||
ErrorData::internal_error(
|
||||
format!("Failed to extract hub version_id: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
(type_str, version_id, true)
|
||||
} else {
|
||||
let path_prefix = extract_path_prefix_from_hashed(&request.name);
|
||||
let favorites_only = scope_config.favorites;
|
||||
let matched_path = if type_str == "script" {
|
||||
find_matching_path(
|
||||
self.backend
|
||||
.list_scripts(&auth, &workspace_id, favorites_only, path_prefix.as_deref())
|
||||
.await
|
||||
.map_err(|e| ErrorData::internal_error(e.message, None))?,
|
||||
&request.name,
|
||||
)
|
||||
} else {
|
||||
find_matching_path(
|
||||
self.backend
|
||||
.list_flows(&auth, &workspace_id, favorites_only, path_prefix.as_deref())
|
||||
.await
|
||||
.map_err(|e| ErrorData::internal_error(e.message, None))?,
|
||||
&request.name,
|
||||
)
|
||||
};
|
||||
|
||||
let matched_path = matched_path.ok_or_else(|| {
|
||||
ErrorData::internal_error(
|
||||
format!(
|
||||
"No {} found matching hashed tool name '{}'",
|
||||
type_str, request.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
|
||||
(type_str, matched_path, false)
|
||||
};
|
||||
|
||||
// Validate script/flow scope
|
||||
if !is_hub && scope_config.granular {
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::schema::{convert_schema_to_schema_type, make_schema_compatible};
|
||||
use crate::common::transform::transform_path;
|
||||
use crate::common::transform::{transform_hub_path, transform_path};
|
||||
use crate::common::types::{
|
||||
FlowInfo, HubScriptInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem,
|
||||
};
|
||||
@@ -17,10 +17,14 @@ use crate::server::backend::McpBackend;
|
||||
|
||||
/// Implementation of ToolableItem for ScriptInfo
|
||||
impl ToolableItem for ScriptInfo {
|
||||
fn get_path_or_id(&self) -> String {
|
||||
fn get_transformed_path(&self) -> String {
|
||||
transform_path(&self.path, "script")
|
||||
}
|
||||
|
||||
fn get_full_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn get_summary(&self) -> &str {
|
||||
self.summary.as_deref().unwrap_or("No summary")
|
||||
}
|
||||
@@ -48,10 +52,14 @@ impl ToolableItem for ScriptInfo {
|
||||
|
||||
/// Implementation of ToolableItem for FlowInfo
|
||||
impl ToolableItem for FlowInfo {
|
||||
fn get_path_or_id(&self) -> String {
|
||||
fn get_transformed_path(&self) -> String {
|
||||
transform_path(&self.path, "flow")
|
||||
}
|
||||
|
||||
fn get_full_path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn get_summary(&self) -> &str {
|
||||
self.summary.as_deref().unwrap_or("No summary")
|
||||
}
|
||||
@@ -79,10 +87,13 @@ impl ToolableItem for FlowInfo {
|
||||
|
||||
/// Implementation of ToolableItem for HubScriptInfo
|
||||
impl ToolableItem for HubScriptInfo {
|
||||
fn get_path_or_id(&self) -> String {
|
||||
let id = self.version_id;
|
||||
fn get_transformed_path(&self) -> String {
|
||||
let summary = self.summary.as_deref().unwrap_or("No summary");
|
||||
format!("hs-{}-{}", id, summary.replace(" ", "_"))
|
||||
transform_hub_path(self.version_id, summary)
|
||||
}
|
||||
|
||||
fn get_full_path(&self) -> &str {
|
||||
self.summary.as_deref().unwrap_or("No summary")
|
||||
}
|
||||
|
||||
fn get_summary(&self) -> &str {
|
||||
@@ -124,7 +135,7 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
|
||||
resources_types: &[ResourceType],
|
||||
) -> Tool {
|
||||
let is_hub = item.is_hub();
|
||||
let path = item.get_path_or_id();
|
||||
let path = item.get_transformed_path();
|
||||
let item_type = item.item_type();
|
||||
let description = format!(
|
||||
"This is a {} named `{}` with the following description: `{}`.{}",
|
||||
@@ -170,15 +181,24 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
|
||||
}
|
||||
};
|
||||
|
||||
let title = {
|
||||
let summary = item.get_summary();
|
||||
if summary == "No summary" {
|
||||
item.get_full_path().to_string()
|
||||
} else {
|
||||
summary.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
Tool {
|
||||
name: Cow::Owned(path),
|
||||
description: Some(Cow::Owned(description)),
|
||||
input_schema: Arc::new(input_schema_map),
|
||||
title: Some(item.get_summary().to_string()),
|
||||
title: Some(title.clone()),
|
||||
output_schema: None,
|
||||
icons: None,
|
||||
annotations: Some(ToolAnnotations {
|
||||
title: Some(item.get_summary().to_string()),
|
||||
title: Some(title),
|
||||
read_only_hint: Some(false), // Can modify environment
|
||||
destructive_hint: Some(true), // Can potentially be destructive
|
||||
idempotent_hint: Some(false), // Are not guaranteed to be idempotent
|
||||
|
||||
@@ -151,6 +151,7 @@ pub struct RunJob {
|
||||
pub args: serde_json::Map<String, serde_json::Value>,
|
||||
pub scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub email: String,
|
||||
pub job_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl From<JobPayload> for RunJob {
|
||||
@@ -160,6 +161,7 @@ impl From<JobPayload> for RunJob {
|
||||
args: Default::default(),
|
||||
scheduled_for_o: None,
|
||||
email: "test@windmill.dev".to_string(),
|
||||
job_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,8 +185,13 @@ impl RunJob {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn job_id(mut self, id: Uuid) -> Self {
|
||||
self.job_id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn push(self, db: &Pool<Postgres>) -> Uuid {
|
||||
let RunJob { payload, args, scheduled_for_o, email } = self;
|
||||
let RunJob { payload, args, scheduled_for_o, email, job_id } = self;
|
||||
let mut hm_args = std::collections::HashMap::new();
|
||||
for (k, v) in args {
|
||||
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
|
||||
@@ -206,7 +213,7 @@ impl RunJob {
|
||||
/* parent_job */ None,
|
||||
/* root job */ None,
|
||||
/* flow_innermost_root_job */ None,
|
||||
/* job_id */ None,
|
||||
/* job_id */ job_id,
|
||||
/* is_flow_step */ false,
|
||||
/* same_worker */ false,
|
||||
None,
|
||||
@@ -230,7 +237,7 @@ impl RunJob {
|
||||
|
||||
/// Push the job as a specific user (for testing permissions)
|
||||
pub async fn push_as(self, db: &Pool<Postgres>, username: &str, email: &str) -> Uuid {
|
||||
let RunJob { payload, args, scheduled_for_o, .. } = self;
|
||||
let RunJob { payload, args, scheduled_for_o, job_id, .. } = self;
|
||||
let mut hm_args = std::collections::HashMap::new();
|
||||
for (k, v) in args {
|
||||
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
|
||||
@@ -252,7 +259,7 @@ impl RunJob {
|
||||
/* parent_job */ None,
|
||||
/* root job */ None,
|
||||
/* flow_innermost_root_job */ None,
|
||||
/* job_id */ None,
|
||||
/* job_id */ job_id,
|
||||
/* is_flow_step */ false,
|
||||
/* same_worker */ false,
|
||||
None,
|
||||
|
||||
@@ -1036,6 +1036,12 @@ pub async fn handle_bun_job(
|
||||
let _ = write_file(job_dir, "main.ts", inner_content)?;
|
||||
} else if !annotation.native && codebase.is_none() {
|
||||
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#)?;
|
||||
} else if codebase.is_some() {
|
||||
// Write a valid fallback package.json for codebase mode. Without this,
|
||||
// nsjail creates an empty 0-byte file (from the mandatory: false mount)
|
||||
// which Node.js fails to parse as JSON (ERR_INVALID_PACKAGE_CONFIG).
|
||||
// If the codebase TAR includes a package.json, it will overwrite this.
|
||||
let _ = write_file(job_dir, "package.json", "{}")?;
|
||||
};
|
||||
|
||||
let common_bun_proc_envs: HashMap<String, String> =
|
||||
|
||||
@@ -4771,6 +4771,7 @@ mount {{
|
||||
|
||||
// Clean up absolute-path symlinks created by setup_volume_mount_paths
|
||||
if !is_sandboxing_enabled() {
|
||||
#[allow(unused_variables)] // state is only used on unix
|
||||
for state in &volume_setup.states {
|
||||
#[cfg(unix)]
|
||||
if state.mount.target.starts_with('/') {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.650.0";
|
||||
export const VERSION = "v1.651.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -39,7 +39,10 @@ export const scriptBootstrapCode = {
|
||||
}
|
||||
`,
|
||||
|
||||
bun: `export async function main() {
|
||||
bun: `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
|
||||
// https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#modes
|
||||
|
||||
export async function main() {
|
||||
return "Hello world";
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -2626,6 +2626,7 @@ export async function push(
|
||||
let [_basePath, changes] = queue.shift()!;
|
||||
const promise = (async () => {
|
||||
const alreadySynced: string[] = [];
|
||||
const deletedVarsResPaths: string[] = [];
|
||||
const isRawApp = isRawAppFile(changes[0].path);
|
||||
if (isRawApp) {
|
||||
const deleteRawApp = changes.find(
|
||||
@@ -2870,12 +2871,23 @@ export async function push(
|
||||
name: change.path.split(SEP)[1],
|
||||
});
|
||||
break;
|
||||
case "resource":
|
||||
await wmill.deleteResource({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".resource.json"),
|
||||
});
|
||||
case "resource": {
|
||||
const resourcePath = removeSuffix(target, ".resource.json");
|
||||
try {
|
||||
await wmill.deleteResource({
|
||||
workspace: workspaceId,
|
||||
path: resourcePath,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e?.status === 404 && deletedVarsResPaths.includes(resourcePath)) {
|
||||
log.debug(`Resource ${resourcePath} already deleted by linked variable`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
deletedVarsResPaths.push(resourcePath);
|
||||
break;
|
||||
}
|
||||
case "resource-type":
|
||||
await wmill.deleteResourceType({
|
||||
workspace: workspaceId,
|
||||
@@ -3012,12 +3024,23 @@ export async function push(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "variable":
|
||||
await wmill.deleteVariable({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(target, ".variable.json"),
|
||||
});
|
||||
case "variable": {
|
||||
const variablePath = removeSuffix(target, ".variable.json");
|
||||
try {
|
||||
await wmill.deleteVariable({
|
||||
workspace: workspaceId,
|
||||
path: variablePath,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e?.status === 404 && deletedVarsResPaths.includes(variablePath)) {
|
||||
log.debug(`Variable ${variablePath} already deleted by linked resource`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
deletedVarsResPaths.push(variablePath);
|
||||
break;
|
||||
}
|
||||
case "user": {
|
||||
const users = await wmill.listUsers({
|
||||
workspace: workspaceId,
|
||||
|
||||
@@ -408,7 +408,8 @@ async function remove(_opts: GlobalOptions, name: string) {
|
||||
|
||||
async function whoami(_opts: GlobalOptions) {
|
||||
await requireLogin(_opts);
|
||||
log.info(await wmill.globalWhoami());
|
||||
const whoamiInfo = await wmill.globalWhoami();
|
||||
log.info(JSON.stringify(whoamiInfo, null, 2));
|
||||
const activeName = await getActiveWorkspaceName(_opts);
|
||||
log.info("Active: " + colors.green.bold(activeName || "none"));
|
||||
}
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export {
|
||||
workspaceAdd,
|
||||
};
|
||||
|
||||
export const VERSION = "1.650.0";
|
||||
export const VERSION = "1.651.1";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Svelte 5 Migration - Bug Report
|
||||
# Testing started: 2026-03-02
|
||||
|
||||
## Warnings (not blocking but worth fixing)
|
||||
|
||||
1. [WARNING] binding_property_non_reactive in Grid.svelte:372:5
|
||||
- `bind:this={moveResizes[item.id]}` is binding to a non-reactive property
|
||||
- File: src/lib/components/apps/svelte-grid/Grid.svelte
|
||||
- Appears multiple times in App editor
|
||||
- Status: NOT FIXED (non-blocking warning)
|
||||
|
||||
2. [WARNING] legacy_recursive_reactive_block in RecomputeAllComponents.svelte
|
||||
- Migrated `$:` reactive block that both accesses and updates the same reactive value
|
||||
- File: src/lib/components/apps/editor/RecomputeAllComponents.svelte
|
||||
- May cause recursive updates when converted to $effect
|
||||
- Status: NOT FIXED (non-blocking warning)
|
||||
|
||||
3. [WARNING] ownership_invalid_mutation in SchemaForm.svelte:70:16
|
||||
- Mutating unbound props (`schema`) is strongly discouraged
|
||||
- Parent: src/lib/components/ApiConnectForm.svelte should use `bind:schema={...}`
|
||||
- Appears when opening PostgreSQL resource creation form
|
||||
- Status: NOT FIXED (non-blocking warning)
|
||||
|
||||
4. [WARNING] ownership_invalid_binding in InputTransformSchemaForm.svelte
|
||||
- Passes `schema` to InputTransformForm.svelte with `bind:`, but parent Pane.svelte didn't declare `schema` as binding
|
||||
- Appears in flow editor when adding a TypeScript step
|
||||
- Status: NOT FIXED (non-blocking warning)
|
||||
|
||||
## Bugs
|
||||
|
||||
1. [BUG] state_descriptors_fixed in Chart.svelte (Queue metrics drawer)
|
||||
- Error: "Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`."
|
||||
- Triggered by: Clicking "Queue metrics" on /workers page
|
||||
- File: src/lib/components/chartjs-wrappers/Chart.svelte
|
||||
- Root cause: Chart.js's `listenArrayEvents` calls Object.defineProperty on data arrays that are Svelte 5 $state proxies, which reject non-standard property descriptors
|
||||
- Fix: Use $state.snapshot() to pass plain copies of data and options to Chart.js
|
||||
- Status: FIXED
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.650.0",
|
||||
"version": "1.651.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.650.0",
|
||||
"version": "1.651.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.650.0",
|
||||
"version": "1.651.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button, Drawer } from './common'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
@@ -6,24 +8,26 @@
|
||||
import AppConnectInner from './AppConnectInner.svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
|
||||
export let expressOAuthSetup = false
|
||||
|
||||
let drawer: Drawer
|
||||
let resourceType = ''
|
||||
let step = 1
|
||||
let disabled = false
|
||||
let isGoogleSignin = false
|
||||
let manual = true
|
||||
|
||||
let appConnectInner: AppConnectInner | undefined = undefined
|
||||
|
||||
let rtToLoad: string | undefined = ''
|
||||
export async function open(rt?: string) {
|
||||
rtToLoad = rt
|
||||
drawer.openDrawer?.()
|
||||
interface Props {
|
||||
expressOAuthSetup?: boolean
|
||||
}
|
||||
|
||||
$: appConnectInner && onRtToLoadChange(rtToLoad)
|
||||
let { expressOAuthSetup = false }: Props = $props()
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
let resourceType = $state('')
|
||||
let step = $state(1)
|
||||
let disabled = $state(false)
|
||||
let isGoogleSignin = $state(false)
|
||||
let manual = $state(true)
|
||||
|
||||
let appConnectInner: AppConnectInner | undefined = $state(undefined)
|
||||
|
||||
let rtToLoad: string | undefined = $state('')
|
||||
export async function open(rt?: string) {
|
||||
rtToLoad = rt
|
||||
drawer?.openDrawer?.()
|
||||
}
|
||||
|
||||
function onRtToLoadChange(rtToLoad: string | undefined) {
|
||||
appConnectInner?.open(rtToLoad)
|
||||
@@ -31,7 +35,10 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let darkMode: boolean = false
|
||||
let darkMode: boolean = $state(false)
|
||||
run(() => {
|
||||
appConnectInner && onRtToLoadChange(rtToLoad)
|
||||
})
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
@@ -47,7 +54,7 @@
|
||||
<DrawerContent
|
||||
title="Add a resource"
|
||||
id="add-resource-drawer"
|
||||
on:close={drawer.closeDrawer}
|
||||
on:close={drawer?.closeDrawer}
|
||||
tooltip="Resources represent connections to third party systems. Learn more on how to integrate external APIs."
|
||||
documentationLink="https://www.windmill.dev/docs/integrations/integrations_on_windmill"
|
||||
>
|
||||
@@ -68,7 +75,7 @@
|
||||
<Button variant="default" on:click={appConnectInner?.back ?? (() => {})}>Back</Button>
|
||||
{/if}
|
||||
{#if isGoogleSignin}
|
||||
<button {disabled} on:click={appConnectInner?.next}>
|
||||
<button {disabled} onclick={appConnectInner?.next}>
|
||||
<img
|
||||
class="h-10 w-auto object-contain"
|
||||
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
|
||||
|
||||
@@ -405,6 +405,7 @@
|
||||
}
|
||||
} else {
|
||||
if (!path) {
|
||||
if (step == 2) return
|
||||
throw Error('Path is not set')
|
||||
}
|
||||
let exists = await VariableService.existsVariable({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
import { onMount, untrack } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
resourceType?: string | undefined
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
let darkMode: boolean = $state(false)
|
||||
|
||||
if (workspace) {
|
||||
$workspaceStore = workspace
|
||||
if (untrack(() => workspace)) {
|
||||
$workspaceStore = untrack(() => workspace)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import AppEditor from './apps/editor/AppEditor.svelte'
|
||||
import type { AppEditorProps } from './apps/types'
|
||||
|
||||
let { app: oldApp, ...props }: AppEditorProps = $props()
|
||||
|
||||
let app = $state(oldApp)
|
||||
let app = $state(untrack(() => oldApp))
|
||||
</script>
|
||||
|
||||
<AppEditor {app} {...props} />
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import SettingCard from './instanceSettings/SettingCard.svelte'
|
||||
|
||||
export let value: any
|
||||
interface Props {
|
||||
value: any;
|
||||
}
|
||||
|
||||
$: enabled = value != undefined
|
||||
let { value = $bindable() }: Props = $props();
|
||||
|
||||
let org = ''
|
||||
|
||||
$: changeOrg(org)
|
||||
let org = $state('')
|
||||
|
||||
|
||||
function changeOrg(org) {
|
||||
if (value) {
|
||||
@@ -30,10 +34,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
let enabled = $derived(value != undefined)
|
||||
run(() => {
|
||||
changeOrg(org)
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="text-xs font-semibold text-emphasis flex gap-4 items-center"
|
||||
><div class="w-[120px]"><IconedResourceType name={'authelia'} after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import SettingCard from './instanceSettings/SettingCard.svelte'
|
||||
|
||||
export let value: any
|
||||
interface Props {
|
||||
value: any;
|
||||
}
|
||||
|
||||
let { value = $bindable() }: Props = $props();
|
||||
|
||||
$: enabled = value != undefined
|
||||
|
||||
// Initialize org from existing auth_url
|
||||
$: org = value?.connect_config?.auth_url?.replace('/application/o/authorize/', '') ?? ''
|
||||
|
||||
$: changeOrg(org)
|
||||
|
||||
function changeOrg(org) {
|
||||
if (value && org) {
|
||||
@@ -30,10 +32,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
let enabled = $derived(value != undefined)
|
||||
// Initialize org from existing auth_url
|
||||
let org = $derived(value?.connect_config?.auth_url?.replace('/application/o/authorize/', '') ?? '')
|
||||
run(() => {
|
||||
changeOrg(org)
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="text-xs font-semibold text-emphasis flex gap-4 items-center"
|
||||
><div class="w-[120px]"><IconedResourceType name={'authentik'} after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
|
||||
@@ -1,38 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import OauthScopes from './OauthScopes.svelte'
|
||||
|
||||
export let connect_config: {
|
||||
interface Props {
|
||||
connect_config?: {
|
||||
scopes: string[]
|
||||
auth_url: string
|
||||
token_url: string
|
||||
req_body_auth: boolean
|
||||
extra_params: { tenant_id: string }
|
||||
extra_params_callback: Record<string, any>
|
||||
} = {
|
||||
};
|
||||
}
|
||||
|
||||
let { connect_config = $bindable({
|
||||
scopes: ['offline_access'],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: true,
|
||||
extra_params: { tenant_id: '' },
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}) }: Props = $props();
|
||||
|
||||
$: if (!connect_config) {
|
||||
connect_config = {
|
||||
scopes: ['offline_access'],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: true,
|
||||
extra_params: { tenant_id: '' },
|
||||
extra_params_callback: {}
|
||||
run(() => {
|
||||
if (!connect_config) {
|
||||
connect_config = {
|
||||
scopes: ['offline_access'],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: true,
|
||||
extra_params: { tenant_id: '' },
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$: if (connect_config.extra_params.tenant_id) {
|
||||
connect_config.auth_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/authorize`
|
||||
connect_config.token_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/token`
|
||||
}
|
||||
run(() => {
|
||||
if (connect_config.extra_params.tenant_id) {
|
||||
connect_config.auth_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/authorize`
|
||||
connect_config.token_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/token`
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<label class="flex flex-col gap-1" for="tenant-id">
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
export let twBgColor = 'bg-blue-200'
|
||||
export let twTextColor = 'text-secondary'
|
||||
export let tooltip: string | undefined = undefined
|
||||
interface Props {
|
||||
twBgColor?: string;
|
||||
twTextColor?: string;
|
||||
tooltip?: string | undefined;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
twBgColor = 'bg-blue-200',
|
||||
twTextColor = 'text-secondary',
|
||||
tooltip = undefined,
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<span class="{twBgColor} {twTextColor} text-2xs rounded px-1 whitespace-nowrap">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{#if tooltip && tooltip != ''}
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
{/if}
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let email: string
|
||||
export let username: string
|
||||
export let isConflict = false
|
||||
export let noPadding = false
|
||||
interface Props {
|
||||
email: string;
|
||||
username: string;
|
||||
isConflict?: boolean;
|
||||
noPadding?: boolean;
|
||||
}
|
||||
|
||||
let loading = false
|
||||
let {
|
||||
email,
|
||||
username = $bindable(),
|
||||
isConflict = false,
|
||||
noPadding = false
|
||||
}: Props = $props();
|
||||
|
||||
let loading = $state(false)
|
||||
|
||||
let usernameInfo:
|
||||
| {
|
||||
@@ -20,7 +29,7 @@
|
||||
username: string
|
||||
}[]
|
||||
}
|
||||
| undefined = undefined
|
||||
| undefined = $state(undefined)
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
const key = event.key
|
||||
@@ -83,7 +92,7 @@
|
||||
<input
|
||||
type="text"
|
||||
class="mb-4"
|
||||
on:keyup={handleKeyUp}
|
||||
onkeyup={handleKeyUp}
|
||||
bind:value={username}
|
||||
disabled={isConflict}
|
||||
/>
|
||||
|
||||
@@ -13,25 +13,25 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let edit: boolean = false
|
||||
let name: string = ''
|
||||
let value: string = ''
|
||||
let edit: boolean = $state(false)
|
||||
let name: string = $state('')
|
||||
let value: string = $state('')
|
||||
|
||||
export function initNew(): void {
|
||||
edit = false
|
||||
name = ''
|
||||
value = ''
|
||||
drawer.openDrawer()
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
export function editVariable(editName: string, editValue: string): void {
|
||||
edit = true
|
||||
name = editName
|
||||
value = editValue
|
||||
drawer.openDrawer()
|
||||
drawer?.openDrawer()
|
||||
}
|
||||
|
||||
let drawer: Drawer
|
||||
let drawer: Drawer | undefined = $state()
|
||||
|
||||
async function updateVariable(): Promise<void> {
|
||||
await WorkspaceService.setEnvironmentVariable({
|
||||
@@ -48,7 +48,7 @@
|
||||
)
|
||||
dispatch('update')
|
||||
|
||||
drawer.closeDrawer()
|
||||
drawer?.closeDrawer()
|
||||
setTimeout(() => {
|
||||
dispatch('update')
|
||||
}, 5000)
|
||||
@@ -58,7 +58,7 @@
|
||||
<Drawer bind:this={drawer} size="900px">
|
||||
<DrawerContent
|
||||
title={edit ? `Update contextual variable ${name}` : 'Create a contextual variable'}
|
||||
on:close={drawer.closeDrawer}
|
||||
on:close={drawer?.closeDrawer}
|
||||
>
|
||||
<div class="flex flex-col gap-8">
|
||||
{#if !edit}
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import OauthExtraParams from './OauthExtraParams.svelte'
|
||||
import OauthScopes from './OauthScopes.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let connect_config = {
|
||||
let { connect_config = $bindable({
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}) } = $props();
|
||||
|
||||
$: if (!connect_config) {
|
||||
connect_config = {
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
run(() => {
|
||||
if (!connect_config) {
|
||||
connect_config = {
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -42,12 +46,12 @@
|
||||
bind:value={connect_config.token_url}
|
||||
/>
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-emphasis font-semibold text-xs">Scopes</span>
|
||||
<OauthScopes bind:scopes={connect_config.scopes} />
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-emphasis font-semibold text-xs"
|
||||
>Extra Query Args for Authorize Request <Tooltip
|
||||
@@ -57,14 +61,14 @@
|
||||
>
|
||||
<OauthExtraParams bind:extra_params={connect_config.extra_params} />
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-emphasis font-semibold text-xs"
|
||||
>Extra Query Args for Token request <Tooltip>Not needed in most cases</Tooltip></span
|
||||
>
|
||||
<OauthExtraParams bind:extra_params={connect_config.extra_params_callback} />
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-emphasis font-semibold text-xs"
|
||||
>Payload <Tooltip
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
|
||||
import type { PopoverPlacement } from './Popover.model'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
interface Props {
|
||||
placement?: PopoverPlacement
|
||||
@@ -33,10 +34,10 @@
|
||||
children,
|
||||
overlay
|
||||
}: Props = $props()
|
||||
const [popperRef, popperContent] = createPopperActions({ placement })
|
||||
const [popperRef, popperContent] = createPopperActions({ placement: untrack(() => placement) })
|
||||
|
||||
const popperOptions: PopperOptions<{}> = {
|
||||
placement,
|
||||
placement: untrack(() => placement),
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import OauthExtraParams from './OauthExtraParams.svelte'
|
||||
import OauthScopes from './OauthScopes.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let login_config = {
|
||||
let { login_config = $bindable({
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
@@ -12,19 +14,21 @@
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}) } = $props();
|
||||
|
||||
$: if (!login_config) {
|
||||
login_config = {
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
userinfo_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
run(() => {
|
||||
if (!login_config) {
|
||||
login_config = {
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
userinfo_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<label class="block pb-6">
|
||||
@@ -51,12 +55,12 @@
|
||||
bind:value={login_config.userinfo_url}
|
||||
/>
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="block pb-6">
|
||||
<span class="text-primary font-semibold text-xs">Scopes</span>
|
||||
<OauthScopes bind:scopes={login_config.scopes} />
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="block pb-6">
|
||||
<span class="text-primary font-semibold text-xs"
|
||||
>Extra Query Args for Authorize Request <Tooltip
|
||||
@@ -66,14 +70,14 @@
|
||||
>
|
||||
<OauthExtraParams bind:extra_params={login_config.extra_params} />
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="block pb-6">
|
||||
<span class="text-primary font-semibold text-xs"
|
||||
>Extra Query Args for Token request <Tooltip>Not needed in most cases</Tooltip></span
|
||||
>
|
||||
<OauthExtraParams bind:extra_params={login_config.extra_params_callback} />
|
||||
</label>
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="block pb-6">
|
||||
<span class="text-primary font-semibold text-xs"
|
||||
>Payload <Tooltip
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" module>
|
||||
import { untrack } from 'svelte'
|
||||
function validate(values: TableEditorValues, dbSchema?: DBSchema) {
|
||||
const columnNamesErrs = values.columns.flatMap((column) => {
|
||||
const isUnique = values.columns.filter((c) => c.name === column.name).length === 1
|
||||
@@ -92,7 +93,7 @@
|
||||
computePreview
|
||||
}: Props = $props()
|
||||
|
||||
const columnTypes = DB_TYPES[dbType]
|
||||
const columnTypes = DB_TYPES[untrack(() => dbType)]
|
||||
const defaultColumnType = (
|
||||
{
|
||||
postgresql: 'BIGSERIAL',
|
||||
@@ -102,10 +103,10 @@
|
||||
mysql: 'varchar',
|
||||
duckdb: 'string'
|
||||
} satisfies Record<DbType, string>
|
||||
)[dbType]
|
||||
)[untrack(() => dbType)]
|
||||
|
||||
const values: TableEditorValues = $state(
|
||||
$state.snapshot(initialValues) ?? {
|
||||
$state.snapshot(untrack(() => initialValues)) ?? {
|
||||
name: '',
|
||||
columns: [],
|
||||
foreignKeys: []
|
||||
@@ -122,8 +123,8 @@
|
||||
...(primaryKey && { primaryKey })
|
||||
})
|
||||
}
|
||||
if (!initialValues) {
|
||||
addColumn({ name: 'id', primaryKey: features?.primaryKeys })
|
||||
if (!untrack(() => initialValues)) {
|
||||
addColumn({ name: 'id', primaryKey: untrack(() => features)?.primaryKeys })
|
||||
}
|
||||
|
||||
const errors: ReturnType<typeof validate> = $derived(validate(values, dbSchema))
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
let randomId = 'datetarget-' + Math.random().toString(36).substring(7)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="flex flex-row gap-1 items-center w-full"
|
||||
id={randomId}
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
let randomId = 'datetarget-' + Math.random().toString(36).substring(7)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="flex flex-row gap-1 items-center w-full relative"
|
||||
id={randomId}
|
||||
|
||||
@@ -6,16 +6,20 @@
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import DefaultScriptsInner from './DefaultScriptsInner.svelte'
|
||||
|
||||
let drawer: Drawer
|
||||
export let placement: 'left' | 'right' = 'left'
|
||||
interface Props {
|
||||
placement?: 'left' | 'right'
|
||||
size?: 'xs3' | 'xs2'
|
||||
noText?: boolean
|
||||
}
|
||||
|
||||
export let size: 'xs3' | 'xs2' = 'xs2'
|
||||
export let noText = false
|
||||
let { placement = 'left', size = 'xs2', noText = false }: Props = $props()
|
||||
|
||||
let drawer: Drawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Drawer bind:this={drawer} {placement}>
|
||||
<DrawerContent title="Edit Default Scripts" on:close={drawer.closeDrawer}>
|
||||
<DrawerContent title="Edit Default Scripts" on:close={drawer?.closeDrawer}>
|
||||
<DefaultScriptsInner />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
import { defaultScriptLanguages } from '$lib/scripts'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
|
||||
export let small = false
|
||||
$: langs = computeLangs($defaultScripts)
|
||||
interface Props {
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
let { small = false }: Props = $props();
|
||||
|
||||
function computeLangs(defaultScripts: WorkspaceDefaultScripts | undefined): Script['language'][] {
|
||||
const allLangs = Object.keys(defaultScriptLanguages) as Script['language'][]
|
||||
@@ -30,6 +33,7 @@
|
||||
requestBody: $defaultScripts
|
||||
})
|
||||
}
|
||||
let langs = $derived(computeLangs($defaultScripts))
|
||||
</script>
|
||||
|
||||
<Alert title="Global to workspace" type="info" class="mb-4" size={small ? 'xs' : 'sm'}>
|
||||
@@ -47,7 +51,7 @@
|
||||
<div>
|
||||
{#if i > 0}
|
||||
<button
|
||||
on:click={() => changePosition(i ?? 0, true)}
|
||||
onclick={() => changePosition(i ?? 0, true)}
|
||||
class={small ? 'mr-2 text-secondary text-sm' : 'text-lg mr-2'}
|
||||
title="Move up"
|
||||
>
|
||||
@@ -56,7 +60,7 @@
|
||||
{/if}
|
||||
{#if i < langs.length - 1}
|
||||
<button
|
||||
on:click={() => changePosition(i ?? 0, false)}
|
||||
onclick={() => changePosition(i ?? 0, false)}
|
||||
class={small ? 'mr-2 text-secondary text-sm' : 'text-lg mr-2'}
|
||||
title="Move down">↓</button
|
||||
>
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let link: string | undefined = undefined
|
||||
|
||||
interface Props {
|
||||
link?: string | undefined;
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { link = undefined, class: className = '', children }: Props = $props();
|
||||
|
||||
</script>
|
||||
|
||||
<div class={twMerge('text-xs text-primary font-normal', $$props.class)}>
|
||||
<slot />
|
||||
<div class={twMerge('text-xs text-primary font-normal', className)}>
|
||||
{@render children?.()}
|
||||
{#if link}
|
||||
<a href={link} target="_blank" class="whitespace-nowrap"
|
||||
>Learn more <ExternalLink size={12} class="inline-block" /></a
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount, setContext, untrack } from 'svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { page } from '$app/state'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte'
|
||||
@@ -169,11 +169,12 @@
|
||||
let loadingCodebaseButton = $state(false)
|
||||
let lastCommandId = ''
|
||||
|
||||
if (initial) {
|
||||
if (initial.type == 'script') {
|
||||
replaceScript(initial.script)
|
||||
} else if (initial.type == 'flow') {
|
||||
replaceFlow(initial.flow)
|
||||
const untrackedInitial = untrack(() => initial)
|
||||
if (untrackedInitial) {
|
||||
if (untrackedInitial.type == 'script') {
|
||||
replaceScript(untrackedInitial.script)
|
||||
} else if (untrackedInitial.type == 'flow') {
|
||||
replaceFlow(untrackedInitial.flow)
|
||||
}
|
||||
modeInitialized = true
|
||||
}
|
||||
@@ -596,9 +597,9 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
let token = $derived($page.url.searchParams.get('wm_token') ?? undefined)
|
||||
let workspace = $derived($page.url.searchParams.get('workspace') ?? undefined)
|
||||
let themeDarkRaw = $derived($page.url.searchParams.get('activeColorTheme'))
|
||||
let token = $derived(page.url.searchParams.get('wm_token') ?? undefined)
|
||||
let workspace = $derived(page.url.searchParams.get('workspace') ?? undefined)
|
||||
let themeDarkRaw = $derived(page.url.searchParams.get('activeColorTheme'))
|
||||
let themeDark = $derived(themeDarkRaw == '2' || themeDarkRaw == '4')
|
||||
|
||||
$effect.pre(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import MenuItem from '$lib/components/meltComponents/MenuItem.svelte'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -18,7 +19,7 @@
|
||||
const {
|
||||
elements: { subTrigger, subMenu },
|
||||
states: { subOpen }
|
||||
} = builders.createSubmenu()
|
||||
} = untrack(() => builders).createSubmenu()
|
||||
|
||||
let subItems = $derived((item.submenuItems ?? []).filter((i) => !i.hide))
|
||||
</script>
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
ids: { menu: dropdownId }
|
||||
} = createDropdownMenu({
|
||||
positioning: {
|
||||
placement
|
||||
placement: untrack(() => placement)
|
||||
},
|
||||
loop: true,
|
||||
onOpenChange: ({ next }) => {
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
import { Hourglass } from 'lucide-svelte'
|
||||
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
|
||||
|
||||
export let duration_ms: number
|
||||
export let self_wait_time_ms: number | undefined = undefined
|
||||
export let aggregate_wait_time_ms: number | undefined = undefined
|
||||
interface Props {
|
||||
duration_ms: number;
|
||||
self_wait_time_ms?: number | undefined;
|
||||
aggregate_wait_time_ms?: number | undefined;
|
||||
}
|
||||
|
||||
let { duration_ms, self_wait_time_ms = undefined, aggregate_wait_time_ms = undefined }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -113,10 +113,10 @@
|
||||
}
|
||||
})
|
||||
|
||||
let lastArgs = $state.snapshot(otherArgs)
|
||||
let lastArgs = $state.snapshot(untrack(() => otherArgs))
|
||||
|
||||
let timeout: number | undefined = $state()
|
||||
let nargs = $state($state.snapshot(otherArgs))
|
||||
let nargs = $state($state.snapshot(untrack(() => otherArgs)))
|
||||
$effect(() => {
|
||||
otherArgs
|
||||
untrack(() => clearTimeout(timeout))
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let jsonView: boolean = $state(customUi?.jsonOnly == true)
|
||||
let jsonView: boolean = $state(untrack(() => customUi)?.jsonOnly == true)
|
||||
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
|
||||
let error: string | undefined = $state(undefined)
|
||||
let editor: SimpleEditor | undefined = $state(undefined)
|
||||
@@ -296,8 +296,8 @@
|
||||
editor?.setCode(schemaString)
|
||||
}
|
||||
|
||||
const editTabDefaultSize = noPreview ? 100 : 50
|
||||
editPanelSize = editTab ? (editPanelInitialSize ?? editTabDefaultSize) : 0
|
||||
const editTabDefaultSize = untrack(() => noPreview) ? 100 : 50
|
||||
editPanelSize = untrack(() => editTab) ? (untrack(() => editPanelInitialSize) ?? editTabDefaultSize) : 0
|
||||
let inputPanelSize = $state(100 - editPanelSize)
|
||||
let editPanelSizeSmooth = tweened(editPanelSize, {
|
||||
duration: 150
|
||||
@@ -592,7 +592,7 @@
|
||||
{argName}
|
||||
{#if !uiOnly}
|
||||
<div onclick={stopPropagation(preventDefault(bubble('click')))}>
|
||||
<Popover placement="bottom-end" containerClasses="p-4" closeButton>
|
||||
<Popover placement="bottom-end" closeButton>
|
||||
{#snippet trigger()}
|
||||
<Button
|
||||
variant="subtle"
|
||||
|
||||
@@ -189,11 +189,11 @@
|
||||
}
|
||||
})
|
||||
|
||||
let lang = $state(scriptLangToEditorLang(scriptLang))
|
||||
let lang = $state(scriptLangToEditorLang(untrack(() => scriptLang)))
|
||||
|
||||
let filePath = $state(computePath(path))
|
||||
let filePath = $state(computePath(untrack(() => path)))
|
||||
|
||||
let initialPath: string | undefined = $state(path)
|
||||
let initialPath: string | undefined = $state(untrack(() => path))
|
||||
|
||||
let websockets: WebSocket[] = []
|
||||
let languageClients: MonacoLanguageClient[] = []
|
||||
@@ -209,7 +209,7 @@
|
||||
let destroyed = false
|
||||
const uri = computeUri(
|
||||
untrack(() => filePath),
|
||||
scriptLang
|
||||
untrack(() => scriptLang)
|
||||
)
|
||||
|
||||
console.log('uri', uri)
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { type Job } from '$lib/gen'
|
||||
import { isScriptPreview } from '$lib/utils'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
export let job: Job | undefined = undefined
|
||||
/** Execution duration of current active job (in ms) */
|
||||
export let executionDuration: number = 0
|
||||
/** Is current job running more than specified value in `longDefinition` seconds */
|
||||
export let longRunning: boolean = false
|
||||
/** What do we count as "long" (in ms)*/
|
||||
export let longDefinition: number = 30_000
|
||||
/** How often component updates execution duration (in ms)
|
||||
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
job?: Job | undefined;
|
||||
/** Execution duration of current active job (in ms) */
|
||||
executionDuration?: number;
|
||||
/** Is current job running more than specified value in `longDefinition` seconds */
|
||||
longRunning?: boolean;
|
||||
/** What do we count as "long" (in ms)*/
|
||||
longDefinition?: number;
|
||||
/** How often component updates execution duration (in ms)
|
||||
* Higher value -> more efficient component is, less accuracy it has
|
||||
* Lower value -> less efficient component is, more accuracy it has
|
||||
*/
|
||||
export let updateResolution: number = 5_000
|
||||
updateResolution?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
job = undefined,
|
||||
executionDuration = $bindable(0),
|
||||
longRunning = $bindable(false),
|
||||
longDefinition = 30_000,
|
||||
updateResolution = 5_000
|
||||
}: Props = $props();
|
||||
|
||||
let startedAt: number | undefined = undefined
|
||||
let busy: boolean = false
|
||||
let busy: boolean = $state(false)
|
||||
let interval: number | undefined
|
||||
// Detect when execution of job started
|
||||
$: if (
|
||||
!busy &&
|
||||
job &&
|
||||
'running' in job &&
|
||||
(job.job_kind == 'script' || isScriptPreview(job?.job_kind))
|
||||
)
|
||||
start(job)
|
||||
|
||||
function start(job: Job) {
|
||||
busy = true
|
||||
@@ -50,4 +58,14 @@
|
||||
// Clear the interval when the component is destroyed
|
||||
clearInterval(interval)
|
||||
})
|
||||
// Detect when execution of job started
|
||||
run(() => {
|
||||
if (
|
||||
!busy &&
|
||||
job &&
|
||||
'running' in job &&
|
||||
(job.job_kind == 'script' || isScriptPreview(job?.job_kind))
|
||||
)
|
||||
start(job)
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<!-- Used to avoid height jitter when loading monaco asynchronously -->
|
||||
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { getOS } from '$lib/utils'
|
||||
import { MONACO_Y_PADDING } from './vscode'
|
||||
|
||||
@@ -45,7 +46,7 @@
|
||||
|
||||
const charWidth = 9 // try to match as closely as possible to monaco editor
|
||||
|
||||
const lineHeight = fontSize * GOLDEN_LINE_HEIGHT_RATIO
|
||||
const lineHeight = untrack(() => fontSize) * GOLDEN_LINE_HEIGHT_RATIO
|
||||
|
||||
let [clientWidth, clientHeight] = $state([0, 0])
|
||||
let showHorizontalScrollbar = $derived(
|
||||
|
||||
@@ -5,19 +5,37 @@
|
||||
import Tooltip from './meltComponents/Tooltip.svelte'
|
||||
import { InfoIcon } from 'lucide-svelte'
|
||||
|
||||
export let label: string
|
||||
export let format: string = ''
|
||||
export let contentEncoding = ''
|
||||
export let type: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let required = false
|
||||
export let displayType: boolean = true
|
||||
export let labelClass: string = ''
|
||||
export let prettify = false
|
||||
export let simpleTooltip: string | undefined = undefined
|
||||
export let lightHeader = false
|
||||
export let SimpleTooltipIcon = InfoIcon
|
||||
export let simpleTooltipIconClass = ''
|
||||
interface Props {
|
||||
label: string;
|
||||
format?: string;
|
||||
contentEncoding?: string;
|
||||
type?: string | undefined;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
displayType?: boolean;
|
||||
labelClass?: string;
|
||||
prettify?: boolean;
|
||||
simpleTooltip?: string | undefined;
|
||||
lightHeader?: boolean;
|
||||
SimpleTooltipIcon?: any;
|
||||
simpleTooltipIconClass?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
label,
|
||||
format = '',
|
||||
contentEncoding = '',
|
||||
type = undefined,
|
||||
disabled = false,
|
||||
required = false,
|
||||
displayType = true,
|
||||
labelClass = '',
|
||||
prettify = false,
|
||||
simpleTooltip = undefined,
|
||||
lightHeader = false,
|
||||
SimpleTooltipIcon = InfoIcon,
|
||||
simpleTooltipIconClass = ''
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="inline-flex flex-row items-baseline truncated">
|
||||
@@ -54,9 +72,11 @@
|
||||
{#if !emptyString(simpleTooltip)}
|
||||
<Tooltip class="ml-2" placement="bottom">
|
||||
<SimpleTooltipIcon size="14" class={'-mb-0.5 ' + simpleTooltipIconClass} />
|
||||
<span class="text-xs" slot="text">
|
||||
{simpleTooltip}
|
||||
</span>
|
||||
{#snippet text()}
|
||||
<span class="text-xs" >
|
||||
{simpleTooltip}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -228,7 +228,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // kept for legacy reasons
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(
|
||||
untrack(() => savedPrimarySchedule)
|
||||
) // kept for legacy reasons
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const simplifiedPoll = writable(false)
|
||||
|
||||
@@ -601,8 +603,8 @@
|
||||
const selectionManager = new SelectionManager()
|
||||
const selectedIdStore = $derived(selectionManager.getSelectedId())
|
||||
// Initialize with selected id if provided
|
||||
if (selectedId) {
|
||||
selectionManager.selectId(selectedId)
|
||||
if (untrack(() => selectedId)) {
|
||||
selectionManager.selectId(untrack(() => selectedId) ?? '')
|
||||
} else {
|
||||
selectionManager.selectId('settings-metadata')
|
||||
}
|
||||
@@ -611,11 +613,11 @@
|
||||
return selectedIdStore
|
||||
}
|
||||
|
||||
const previewArgsStore = $state({ val: initialArgs })
|
||||
const previewArgsStore = $state({ val: untrack(() => initialArgs) })
|
||||
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
|
||||
const flowEditorDrawer = writable<FlowEditorDrawer | undefined>(undefined)
|
||||
const history = initHistory(flowStore.val)
|
||||
const pathStore = writable<string>(pathStoreInit ?? initialPath)
|
||||
const history = initHistory(untrack(() => flowStore).val)
|
||||
const pathStore = writable<string>(untrack(() => pathStoreInit) ?? initialPath)
|
||||
const captureOn = writable<boolean>(false)
|
||||
const showCaptureHint = writable<boolean | undefined>(undefined)
|
||||
const flowInputEditorStateStore = writable<FlowInputEditorState>({
|
||||
@@ -642,15 +644,15 @@
|
||||
scriptEditorDrawer,
|
||||
flowEditorDrawer,
|
||||
history,
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
flowStateStore: untrack(() => flowStateStore),
|
||||
flowStore: untrack(() => flowStore),
|
||||
pathStore,
|
||||
stepsInputArgs,
|
||||
saveDraft,
|
||||
initialPathStore,
|
||||
fakeInitialPath,
|
||||
flowInputsStore: writable<FlowInput>({}),
|
||||
customUi,
|
||||
customUi: untrack(() => customUi),
|
||||
insertButtonOpen,
|
||||
executionCount: writable(0),
|
||||
flowInputEditorState: flowInputEditorStateStore,
|
||||
@@ -661,10 +663,13 @@
|
||||
})
|
||||
|
||||
// Set up NoteEditor context for note editing capabilities
|
||||
const noteEditor = new NoteEditor(flowStore, () => {
|
||||
// Enable notes display when a note is created
|
||||
flowEditor?.enableNotes?.()
|
||||
})
|
||||
const noteEditor = new NoteEditor(
|
||||
untrack(() => flowStore),
|
||||
() => {
|
||||
// Enable notes display when a note is created
|
||||
flowEditor?.enableNotes?.()
|
||||
}
|
||||
)
|
||||
setNoteEditorContext(noteEditor)
|
||||
|
||||
setContext(
|
||||
@@ -678,9 +683,9 @@
|
||||
[
|
||||
{ type: 'webhook', path: '', isDraft: false },
|
||||
{ type: 'default_email', path: '', isDraft: false },
|
||||
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
|
||||
...(untrack(() => draftTriggersFromUrl) ?? savedFlow?.draft?.draft_triggers ?? [])
|
||||
],
|
||||
selectedTriggerIndexFromUrl,
|
||||
untrack(() => selectedTriggerIndexFromUrl),
|
||||
saveSessionDraft
|
||||
)
|
||||
)
|
||||
@@ -804,7 +809,7 @@
|
||||
onClick: () => void
|
||||
}> = []
|
||||
|
||||
if (customUi.topBar?.extraDeployOptions != false) {
|
||||
if (untrack(() => customUi).topBar?.extraDeployOptions != false) {
|
||||
if (savedFlow?.draft_only === false || savedFlow?.draft_only === undefined) {
|
||||
dropdownItems.push({
|
||||
label: 'Exit & see details',
|
||||
@@ -812,14 +817,14 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (!newFlow) {
|
||||
if (!untrack(() => newFlow)) {
|
||||
dropdownItems.push({
|
||||
label: 'Fork',
|
||||
onClick: () => window.open(`/flows/add?template=${initialPath}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (!newFlow && !isCloudHosted() && !isRuleActive('DisableWorkspaceForking')) {
|
||||
if (!untrack(() => newFlow) && !isCloudHosted() && !isRuleActive('DisableWorkspaceForking')) {
|
||||
dropdownItems.push({
|
||||
label: 'Edit in workspace fork',
|
||||
onClick: () => window.open(buildForkEditUrl('flow', initialPath))
|
||||
@@ -1036,10 +1041,10 @@
|
||||
}
|
||||
|
||||
let stepHistoryLoader = new StepHistoryLoader(
|
||||
loadedFromHistoryFromUrl?.stepsState ?? {},
|
||||
loadedFromHistoryFromUrl?.flowJobInitial,
|
||||
untrack(() => loadedFromHistoryFromUrl)?.stepsState ?? {},
|
||||
untrack(() => loadedFromHistoryFromUrl)?.flowJobInitial,
|
||||
saveSessionDraft,
|
||||
noInitial
|
||||
untrack(() => noInitial)
|
||||
)
|
||||
setStepHistoryLoaderContext(stepHistoryLoader)
|
||||
|
||||
|
||||
@@ -9,23 +9,38 @@
|
||||
import { dfs } from './flows/dfs'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let flow: {
|
||||
|
||||
interface Props {
|
||||
flow: {
|
||||
summary: string
|
||||
description?: string
|
||||
value: FlowValue
|
||||
schema?: any
|
||||
path?: string
|
||||
};
|
||||
overflowAuto?: boolean;
|
||||
noSide?: boolean;
|
||||
download?: boolean;
|
||||
noGraph?: boolean;
|
||||
triggerNode?: boolean;
|
||||
stepDetail?: FlowModule | string | undefined;
|
||||
workspace?: string | undefined;
|
||||
minHeight?: number;
|
||||
noBorder?: boolean;
|
||||
}
|
||||
|
||||
export let overflowAuto = false
|
||||
export let noSide = false
|
||||
export let download = false
|
||||
export let noGraph = false
|
||||
export let triggerNode = false
|
||||
export let stepDetail: FlowModule | string | undefined = undefined
|
||||
export let workspace: string | undefined = $workspaceStore
|
||||
export let minHeight = 400
|
||||
export let noBorder = false
|
||||
let {
|
||||
flow,
|
||||
overflowAuto = false,
|
||||
noSide = false,
|
||||
download = false,
|
||||
noGraph = false,
|
||||
triggerNode = false,
|
||||
stepDetail = $bindable(undefined),
|
||||
workspace = $workspaceStore,
|
||||
minHeight = 400,
|
||||
noBorder = false
|
||||
}: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -19,17 +19,20 @@
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
|
||||
|
||||
export let schema: any | undefined = undefined
|
||||
interface Props {
|
||||
schema?: any | undefined
|
||||
stepDetail?: FlowModule | string | undefined
|
||||
jobScriptHash?: string | undefined
|
||||
}
|
||||
|
||||
export let stepDetail: FlowModule | string | undefined = undefined
|
||||
export let jobScriptHash: string | undefined = undefined
|
||||
let codeViewer: Drawer
|
||||
let { schema = undefined, stepDetail = undefined, jobScriptHash = undefined }: Props = $props()
|
||||
let codeViewer: Drawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<Drawer bind:this={codeViewer} size="900px">
|
||||
<DrawerContent title={'Expanded Code'} on:close={codeViewer.closeDrawer}>
|
||||
<DrawerContent title={'Expanded Code'} on:close={codeViewer?.closeDrawer}>
|
||||
{#if stepDetail && typeof stepDetail != 'string'}
|
||||
{#if stepDetail.value.type == 'script'}
|
||||
<div class="mb-4">
|
||||
@@ -183,7 +186,7 @@
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="subtle"
|
||||
onClick={codeViewer.openDrawer}
|
||||
onClick={codeViewer?.openDrawer}
|
||||
startIcon={{ icon: Expand }}>Expand</Button
|
||||
>
|
||||
</div>
|
||||
@@ -221,7 +224,7 @@
|
||||
<Button
|
||||
unifiedSize="sm"
|
||||
variant="subtle"
|
||||
onClick={codeViewer.openDrawer}
|
||||
onClick={codeViewer?.openDrawer}
|
||||
startIcon={{ icon: Expand }}>Expand</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
|
||||
export let schema: Schema | { [key: string]: unknown } | undefined
|
||||
interface Props {
|
||||
schema: Schema | { [key: string]: unknown } | undefined;
|
||||
}
|
||||
|
||||
let { schema }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ul class="my-2">
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
const timelineItems = $derived(timelineCompute?.items ?? undefined)
|
||||
const timelineNow = $derived(timelineCompute?.now ?? Date.now())
|
||||
|
||||
let moduleTracker = new ChangeTracker($state.snapshot(job.raw_flow?.modules ?? []))
|
||||
let moduleTracker = new ChangeTracker($state.snapshot(untrack(() => job).raw_flow?.modules ?? []))
|
||||
$effect(() => {
|
||||
readFieldsRecursively(job.raw_flow?.modules ?? [])
|
||||
untrack(() => moduleTracker.track($state.snapshot(job.raw_flow?.modules ?? [])))
|
||||
@@ -123,7 +123,7 @@
|
||||
}
|
||||
|
||||
let timelineAvailableWidths = $state<Record<string, number>>({})
|
||||
let lastJobId: string | undefined = $state(job.id)
|
||||
let lastJobId: string | undefined = $state(untrack(() => job).id)
|
||||
|
||||
const timelinelWidth = $derived.by(() => {
|
||||
const widths = Object.values(timelineAvailableWidths)
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
import AnimatedButton from './common/button/AnimatedButton.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let connecting: boolean
|
||||
export let id: undefined | string = undefined
|
||||
export let wrapperClasses = ''
|
||||
interface Props {
|
||||
connecting: boolean;
|
||||
id?: undefined | string;
|
||||
wrapperClasses?: string;
|
||||
}
|
||||
|
||||
let { connecting, id = undefined, wrapperClasses = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AnimatedButton animate={connecting} baseRadius="6px" animationDuration="2s" marginWidth="2px">
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
showLogsWithResult = false
|
||||
}: Props = $props()
|
||||
|
||||
let lastJobId: string = jobId
|
||||
let lastJobId: string = untrack(() => jobId)
|
||||
|
||||
let retryStatus = $state({ val: {} })
|
||||
let globalRefreshes: Record<string, ((clear, root) => Promise<void>)[]> = $state({})
|
||||
@@ -71,11 +71,11 @@
|
||||
flowState,
|
||||
suspendStatus,
|
||||
retryStatus,
|
||||
hideDownloadInGraph,
|
||||
hideNodeDefinition,
|
||||
hideTimeline,
|
||||
hideJobId,
|
||||
hideDownloadLogs
|
||||
hideDownloadInGraph: untrack(() => hideDownloadInGraph),
|
||||
hideNodeDefinition: untrack(() => hideNodeDefinition),
|
||||
hideTimeline: untrack(() => hideTimeline),
|
||||
hideJobId: untrack(() => hideJobId),
|
||||
hideDownloadLogs: untrack(() => hideDownloadLogs)
|
||||
})
|
||||
|
||||
function loadOwner(path: string) {
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
|
||||
let resultStreams: Record<string, string | undefined> = $state({})
|
||||
|
||||
if (onResultStreamUpdate == undefined) {
|
||||
if (untrack(() => onResultStreamUpdate) == undefined) {
|
||||
onResultStreamUpdate = ({
|
||||
jobId,
|
||||
result_stream
|
||||
@@ -234,7 +234,7 @@
|
||||
})
|
||||
|
||||
let jobResults: any[] = $state(
|
||||
flowJobIds?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
|
||||
untrack(() => flowJobIds)?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
|
||||
)
|
||||
|
||||
let retry_selected = $state('')
|
||||
@@ -805,7 +805,7 @@
|
||||
|
||||
let destroyed = false
|
||||
|
||||
updateRecursiveRefresh(jobId)
|
||||
updateRecursiveRefresh(untrack(() => jobId))
|
||||
|
||||
async function updateJobId() {
|
||||
if (jobId !== job?.id || innerModules == undefined) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { type FlowValue, FlowService } from '$lib/gen'
|
||||
import { Tab, Tabs, TabContent } from './common'
|
||||
import SchemaViewer from './SchemaViewer.svelte'
|
||||
@@ -51,13 +52,14 @@
|
||||
}: Props = $props()
|
||||
|
||||
let open: { [id: number]: boolean } = {}
|
||||
if (initialOpen) {
|
||||
open[initialOpen] = true
|
||||
const untrackedInitialOpen = untrack(() => initialOpen)
|
||||
if (untrackedInitialOpen) {
|
||||
open[untrackedInitialOpen] = true
|
||||
}
|
||||
|
||||
let previousVersionId: number | undefined = $state(undefined)
|
||||
let previousFlow: PreviousFlow | undefined = $state(undefined)
|
||||
let tab: TabValue = $state(initTab ?? 'diff')
|
||||
let tab: TabValue = $state(untrack(() => initTab) ?? 'diff')
|
||||
|
||||
let previousFlowCache: Record<number, PreviousFlow> = {}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import AiChatLayout from './copilot/chat/AiChatLayout.svelte'
|
||||
import type { FlowBuilderProps } from './flow_builder'
|
||||
import FlowBuilder from './FlowBuilder.svelte'
|
||||
@@ -11,12 +12,12 @@
|
||||
...props
|
||||
}: FlowBuilderProps & { light?: boolean } = $props()
|
||||
|
||||
let flowStore = $state(oldFlowStore)
|
||||
let flowStateStore = $state(oldFlowStateStore)
|
||||
let flowStore = $state(untrack(() => oldFlowStore))
|
||||
let flowStateStore = $state(untrack(() => oldFlowStateStore))
|
||||
|
||||
let trialRender = $state(true)
|
||||
|
||||
if (light) {
|
||||
if (untrack(() => light)) {
|
||||
setTimeout(() => {
|
||||
trialRender = false
|
||||
}, 1000 * 300)
|
||||
|
||||
@@ -270,12 +270,14 @@
|
||||
{/if}
|
||||
{#if perms}
|
||||
<TableCustom>
|
||||
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
|
||||
<tr slot="header-row">
|
||||
<th>user/group</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
{#snippet headerRow()}
|
||||
<tr >
|
||||
<th>user/group</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{/snippet}
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each perms ?? [] as { owner_name, role }}<tr>
|
||||
@@ -407,7 +409,7 @@
|
||||
<p class="text-primary text-sm">No folder is managing this folder</p>
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<tr slot="headerRow">
|
||||
<th>folder</th>
|
||||
<th />
|
||||
</tr>
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { FolderService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Cell from './table/Cell.svelte'
|
||||
|
||||
export let name: string
|
||||
export let tabular = false
|
||||
export let order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources']
|
||||
interface Props {
|
||||
name: string;
|
||||
tabular?: boolean;
|
||||
order?: any;
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadUsage()
|
||||
let { name, tabular = false, order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources'] }: Props = $props();
|
||||
|
||||
let usage: Record<string, number> = {}
|
||||
|
||||
let usage: Record<string, number> = $state({})
|
||||
|
||||
async function loadUsage() {
|
||||
usage = await FolderService.getFolderUsage({ workspace: $workspaceStore!, name })
|
||||
}
|
||||
run(() => {
|
||||
$workspaceStore && loadUsage()
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if tabular}
|
||||
|
||||
@@ -5,10 +5,17 @@
|
||||
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
let divEl: HTMLDivElement | null = $state(null)
|
||||
let editor: meditor.IStandaloneCodeEditor
|
||||
|
||||
export let code: string = ''
|
||||
|
||||
interface Props {
|
||||
code?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { code = '', class: className = '' }: Props = $props();
|
||||
|
||||
|
||||
async function loadMonaco() {
|
||||
editor = meditor.create(divEl as HTMLDivElement, {
|
||||
@@ -43,4 +50,4 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={divEl} class="{$$props.class ?? ''} editor"></div>
|
||||
<div bind:this={divEl} class="{className} editor"></div>
|
||||
|
||||
@@ -159,12 +159,14 @@
|
||||
{/if}
|
||||
{#if members}
|
||||
<TableCustom>
|
||||
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
|
||||
<tr slot="header-row">
|
||||
<th>user</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
||||
{#snippet headerRow()}
|
||||
<tr >
|
||||
<th>user</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{/snippet}
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each members ?? [] as { member_name, role }}<tr>
|
||||
@@ -301,10 +303,12 @@
|
||||
{#if instance_group?.emails}
|
||||
<h2 class="mt-6 text-emphasis text-xs font-semibold">Members from the instance group</h2>
|
||||
<TableCustom>
|
||||
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
|
||||
<tr slot="header-row">
|
||||
<th>user</th>
|
||||
</tr>
|
||||
|
||||
{#snippet headerRow()}
|
||||
<tr >
|
||||
<th>user</th>
|
||||
</tr>
|
||||
{/snippet}
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each instance_group?.emails ?? [] as email}<tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { stopPropagation, createBubbler } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
@@ -34,7 +35,7 @@
|
||||
}: Props = $props()
|
||||
|
||||
let error = $state('')
|
||||
const regex = acceptUnderScores ? /^[a-zA-Z][a-zA-Z0-9_]*$/ : /^[a-zA-Z][a-zA-Z0-9]*$/
|
||||
const regex = untrack(() => acceptUnderScores) ? /^[a-zA-Z][a-zA-Z0-9_]*$/ : /^[a-zA-Z][a-zA-Z0-9]*$/
|
||||
|
||||
function validateId(id: string, reservedIds: string[], reservedPrefixes: string[]) {
|
||||
if (id == initialId) {
|
||||
|
||||
@@ -407,6 +407,7 @@
|
||||
}
|
||||
|
||||
function updatePropsBeingEdited(focused: boolean) {
|
||||
if (!exprBeingEdited) return
|
||||
let newPropsBeingEdited = [...$exprBeingEdited]
|
||||
if (focused) {
|
||||
newPropsBeingEdited.push(argName)
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
import Cell from './table/Cell.svelte'
|
||||
import Row from './table/Row.svelte'
|
||||
|
||||
export let inputTransforms: Record<string, InputTransform>
|
||||
$: entries = Object.entries(inputTransforms)
|
||||
interface Props {
|
||||
inputTransforms: Record<string, InputTransform>;
|
||||
}
|
||||
|
||||
let { inputTransforms }: Props = $props();
|
||||
let entries = $derived(Object.entries(inputTransforms))
|
||||
</script>
|
||||
|
||||
{#if entries.length}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { GroupService, type InstanceGroup } from '$lib/gen'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
@@ -8,17 +10,18 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
|
||||
export let name: string
|
||||
interface Props {
|
||||
name: string;
|
||||
}
|
||||
|
||||
let email = ''
|
||||
let instance_group: InstanceGroup | undefined
|
||||
let members: { member_email: string }[] | undefined = undefined
|
||||
let { name }: Props = $props();
|
||||
|
||||
let email = $state('')
|
||||
let instance_group: InstanceGroup | undefined = $state()
|
||||
let members: { member_email: string }[] | undefined = $state(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: {
|
||||
load()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
return Promise.all([loadInstanceGroup()])
|
||||
@@ -32,6 +35,9 @@
|
||||
})
|
||||
: []
|
||||
}
|
||||
run(() => {
|
||||
load()
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
@@ -85,17 +91,20 @@
|
||||
</div>
|
||||
{#if members}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>user</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each members as { member_email }}<tr>
|
||||
<td>{member_email}</td>
|
||||
<td>
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
on:click={async () => {
|
||||
{#snippet headerRow()}
|
||||
<tr >
|
||||
<th>user</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{/snippet}
|
||||
{#snippet body()}
|
||||
<tbody >
|
||||
{#each members as { member_email }}<tr>
|
||||
<td>{member_email}</td>
|
||||
<td>
|
||||
<button
|
||||
class="ml-2 text-red-500"
|
||||
onclick={async () => {
|
||||
await GroupService.removeUserFromInstanceGroup({
|
||||
name,
|
||||
requestBody: { email: member_email }
|
||||
@@ -104,10 +113,11 @@
|
||||
sendUserToast('User removed')
|
||||
loadInstanceGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
</td>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
>
|
||||
</td>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
{/snippet}
|
||||
</TableCustom>
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { stopPropagation, createBubbler } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
import { Pencil } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
@@ -9,13 +12,23 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
|
||||
export let value: string | undefined
|
||||
export let email: string
|
||||
export let username: string | undefined = undefined
|
||||
export let automateUsernameCreation: boolean = false
|
||||
export let login_type: string
|
||||
interface Props {
|
||||
value: string | undefined
|
||||
email: string
|
||||
username?: string | undefined
|
||||
automateUsernameCreation?: boolean
|
||||
login_type: string
|
||||
}
|
||||
|
||||
let password: string = ''
|
||||
let {
|
||||
value = $bindable(),
|
||||
email,
|
||||
username = undefined,
|
||||
automateUsernameCreation = false,
|
||||
login_type = $bindable()
|
||||
}: Props = $props()
|
||||
|
||||
let password: string = $state('')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -46,12 +59,12 @@
|
||||
}}
|
||||
closeButton
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
{#snippet trigger()}
|
||||
<Button unifiedSize="sm" nonCaptureEvent={true} variant="subtle" startIcon={{ icon: Pencil }}
|
||||
>Edit</Button
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-col gap-8 max-w-sm p-4">
|
||||
{#if automateUsernameCreation && username}
|
||||
<ChangeInstanceUsernameInner {email} {username} on:renamed noPadding />
|
||||
@@ -96,10 +109,11 @@
|
||||
type="password"
|
||||
bind:value={password}
|
||||
class="!w-auto grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
onclick={stopPropagation(() => {})}
|
||||
onkeydown={stopPropagation(bubble('keydown'))}
|
||||
onkeypress={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Enter') {
|
||||
savePassword()
|
||||
}
|
||||
}}
|
||||
@@ -126,10 +140,11 @@
|
||||
type="text"
|
||||
bind:value={login_type}
|
||||
class="!w-auto grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
on:keydown|stopPropagation
|
||||
on:keypress|stopPropagation={({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
onclick={stopPropagation(() => {})}
|
||||
onkeydown={stopPropagation(bubble('keydown'))}
|
||||
onkeypress={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Enter') {
|
||||
saveLoginType()
|
||||
}
|
||||
}}
|
||||
@@ -153,5 +168,5 @@
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</Popover>
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import { globalEmailInvite } from '$lib/stores'
|
||||
|
||||
export let close: (() => void) | undefined = undefined
|
||||
interface Props {
|
||||
close?: (() => void) | undefined;
|
||||
}
|
||||
|
||||
let { close = undefined }: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let is_super_admin = false
|
||||
let password: string = generateRandomString(10)
|
||||
let name: string | undefined
|
||||
let is_super_admin = $state(false)
|
||||
let password: string = $state(generateRandomString(10))
|
||||
let name: string | undefined = $state()
|
||||
let company: string | undefined
|
||||
|
||||
async function addUser() {
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
let lastStartedAt: number = Date.now()
|
||||
let currentId: string | undefined = $state(undefined)
|
||||
let noPingTimeout: number | undefined = undefined
|
||||
let lastNoLogs = $state(noLogs)
|
||||
let lastNoLogs = $state(untrack(() => noLogs))
|
||||
let lastCompletedJobId = $state<string | undefined>(undefined)
|
||||
|
||||
let token = getContext<{ token?: string }>('AuthToken')
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import SettingCard from './instanceSettings/SettingCard.svelte'
|
||||
|
||||
export let value: any
|
||||
interface Props {
|
||||
value: any;
|
||||
}
|
||||
|
||||
let { value = $bindable() }: Props = $props();
|
||||
|
||||
const AUTH_URL_SUFFIX = '/ui/oauth2'
|
||||
|
||||
$: enabled = value != undefined
|
||||
|
||||
// If `baseUrl` is not already set in the form, try to parse it from the `auth_url` value
|
||||
//
|
||||
// The binding dance here allows us to avoid rendering the string 'undefined' in the input, and
|
||||
// also allow lazy/async binding of the `value` prop.
|
||||
$: derivedBaseUrl = value?.connect_config?.auth_url?.replace(AUTH_URL_SUFFIX, '')
|
||||
let proxyUrlValue = undefined
|
||||
$: baseUrl = proxyUrlValue ?? derivedBaseUrl ?? ''
|
||||
let proxyUrlValue = $state(undefined)
|
||||
|
||||
$: changeValues({ baseUrl, id: value?.id ?? '' })
|
||||
|
||||
function changeValues({ baseUrl, id }) {
|
||||
if (value) {
|
||||
@@ -40,10 +38,20 @@
|
||||
proxyUrlValue = baseUrl
|
||||
}
|
||||
}
|
||||
let enabled = $derived(value != undefined)
|
||||
// If `baseUrl` is not already set in the form, try to parse it from the `auth_url` value
|
||||
//
|
||||
// The binding dance here allows us to avoid rendering the string 'undefined' in the input, and
|
||||
// also allow lazy/async binding of the `value` prop.
|
||||
let derivedBaseUrl = $derived(value?.connect_config?.auth_url?.replace(AUTH_URL_SUFFIX, ''))
|
||||
let baseUrl = $derived(proxyUrlValue ?? derivedBaseUrl ?? '')
|
||||
run(() => {
|
||||
changeValues({ baseUrl, id: value?.id ?? '' })
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="text-xs font-semibold text-emphasis flex gap-4 items-center"
|
||||
><div class="w-[120px]"><IconedResourceType name={'kanidm'} after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
|
||||
@@ -63,7 +63,10 @@
|
||||
<span class="text-secondary font-normal text-xs"
|
||||
>{'REALM_URL/protocol/openid-connect/auth'}</span
|
||||
>
|
||||
<TextInput inputProps={{ type: 'text', placeholder: 'yourorg' }} bind:value={value['org']} />
|
||||
<TextInput
|
||||
inputProps={{ type: 'text', placeholder: 'yourorg' }}
|
||||
bind:value={value['org']}
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-emphasis font-semibold text-xs">Custom Name</span>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let id: string
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
$: id && console.log('updateJobId')
|
||||
interface Props {
|
||||
id: string;
|
||||
}
|
||||
|
||||
let { id }: Props = $props();
|
||||
|
||||
run(() => {
|
||||
id && console.log('updateJobId')
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte'
|
||||
import { AnsiUp } from 'ansi_up'
|
||||
|
||||
export let content: string
|
||||
export let highlighted: any[]
|
||||
interface Props {
|
||||
content: string
|
||||
highlighted: any[]
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
let { content, highlighted, onClick }: Props = $props()
|
||||
|
||||
const ansi_up = new AnsiUp()
|
||||
ansi_up.use_classes = true
|
||||
@@ -27,10 +33,10 @@
|
||||
return html2
|
||||
}
|
||||
|
||||
let html = highlightSnippet(content)
|
||||
let html = highlightSnippet(untrack(() => content))
|
||||
</script>
|
||||
|
||||
<button on:click class="font-light !m-0 !p-0">
|
||||
<button onclick={onClick} class="font-light !m-0 !p-0">
|
||||
<pre
|
||||
class="bg-surface-secondary hover:bg-surface px-2 py-1 text-secondary text-xs w-[100%] whitespace-pre border min-w-full text-start">
|
||||
{@html html}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" module>
|
||||
import { untrack } from 'svelte'
|
||||
const s3LogPrefixes = [
|
||||
'[windmill] Previous logs have been saved to object storage at logs/',
|
||||
'[windmill] Previous logs have been saved to disk at logs/',
|
||||
@@ -73,7 +74,7 @@
|
||||
let LOG_INC = 10000
|
||||
let LOG_LIMIT = $state(LOG_INC)
|
||||
|
||||
let lastJobId = $state(jobId)
|
||||
let lastJobId = $state(untrack(() => jobId))
|
||||
|
||||
let loadedFromObjectStore = $state('')
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
export async function refresh() {
|
||||
await getInstance()?.update()
|
||||
@@ -26,7 +27,9 @@
|
||||
content
|
||||
}: Props = $props()
|
||||
|
||||
const [popperRef, popperContent, getInstance] = createPopperActions({ placement })
|
||||
const [popperRef, popperContent, getInstance] = createPopperActions({
|
||||
placement: untrack(() => placement)
|
||||
})
|
||||
|
||||
export function open() {
|
||||
showTooltip = true
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { Map, View, Feature } from 'ol'
|
||||
import { Fill, Stroke, Style, Text } from 'ol/style.js'
|
||||
import { useGeographic } from 'ol/proj.js'
|
||||
@@ -18,17 +20,26 @@
|
||||
strokeColor?: string
|
||||
}
|
||||
|
||||
export let lon: number | undefined = undefined
|
||||
export let lat: number | undefined = undefined
|
||||
export let zoom: number | undefined = undefined
|
||||
export let markers: Marker[] | string | undefined = undefined
|
||||
interface Props {
|
||||
lon?: number | undefined;
|
||||
lat?: number | undefined;
|
||||
zoom?: number | undefined;
|
||||
markers?: Marker[] | string | undefined;
|
||||
}
|
||||
|
||||
let {
|
||||
lon = undefined,
|
||||
lat = undefined,
|
||||
zoom = undefined,
|
||||
markers = undefined
|
||||
}: Props = $props();
|
||||
|
||||
const LAYER_NAME = {
|
||||
MARKER: 'Marker'
|
||||
} as const
|
||||
|
||||
let map: Map | undefined = undefined
|
||||
let mapElement: HTMLDivElement | undefined = undefined
|
||||
let map: Map | undefined = $state(undefined)
|
||||
let mapElement: HTMLDivElement | undefined = $state(undefined)
|
||||
|
||||
function getLayersByName(name: keyof typeof LAYER_NAME) {
|
||||
return map
|
||||
@@ -100,36 +111,38 @@
|
||||
createMarkerLayers()?.forEach((l) => map?.addLayer(l))
|
||||
}
|
||||
|
||||
$: if (!map && mapElement) {
|
||||
useGeographic()
|
||||
map = new Map({
|
||||
target: mapElement,
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM()
|
||||
run(() => {
|
||||
if (!map && mapElement) {
|
||||
useGeographic()
|
||||
map = new Map({
|
||||
target: mapElement,
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM()
|
||||
}),
|
||||
...(createMarkerLayers() || [])
|
||||
],
|
||||
view: new View({
|
||||
center: [lon ?? 0, lat ?? 0],
|
||||
zoom: zoom ?? 2
|
||||
}),
|
||||
...(createMarkerLayers() || [])
|
||||
],
|
||||
view: new View({
|
||||
center: [lon ?? 0, lat ?? 0],
|
||||
zoom: zoom ?? 2
|
||||
}),
|
||||
controls: defaultControls({
|
||||
attribution: false
|
||||
controls: defaultControls({
|
||||
attribution: false
|
||||
})
|
||||
})
|
||||
})
|
||||
if (lat && lon) {
|
||||
map.getView().setCenter([lon, lat])
|
||||
}
|
||||
if (lat && lon) {
|
||||
map.getView().setCenter([lon, lat])
|
||||
}
|
||||
|
||||
if (map && zoom) {
|
||||
map.getView().setZoom(zoom)
|
||||
}
|
||||
if (map && zoom) {
|
||||
map.getView().setZoom(zoom)
|
||||
}
|
||||
|
||||
if (map && markers) {
|
||||
updateMarkers()
|
||||
if (map && markers) {
|
||||
updateMarkers()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={mapElement} class="w-full h-[300px]"></div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { type MetricDataPoint, MetricsService } from '$lib/gen'
|
||||
import { displayTime } from '$lib/utils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -17,16 +19,20 @@
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale)
|
||||
|
||||
export let jobId: string
|
||||
export let jobUpdateLastFetch: Date | undefined
|
||||
interface Props {
|
||||
jobId: string;
|
||||
jobUpdateLastFetch: Date | undefined;
|
||||
}
|
||||
|
||||
let { jobId, jobUpdateLastFetch }: Props = $props();
|
||||
|
||||
let jobMetricsLastFetch: Date | undefined = undefined
|
||||
let jobMemoryStats: MetricDataPoint[] | undefined = undefined
|
||||
let jobMemoryStats: MetricDataPoint[] | undefined = $state(undefined)
|
||||
|
||||
let data: {
|
||||
x: number
|
||||
y: number
|
||||
}[] = []
|
||||
}[] = $state([])
|
||||
let labels: string[] = []
|
||||
|
||||
async function loadMetricsData() {
|
||||
@@ -66,7 +72,9 @@
|
||||
data = [...data]
|
||||
}
|
||||
|
||||
$: jobUpdateLastFetch && loadMetricsData()
|
||||
run(() => {
|
||||
jobUpdateLastFetch && loadMetricsData()
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative max-h-100">
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
import { displayDate } from '$lib/utils'
|
||||
import { Hourglass } from 'lucide-svelte'
|
||||
|
||||
export let type: FlowStatusModule['type']
|
||||
export let scheduled_for: Date | undefined
|
||||
export let skipped: boolean = false
|
||||
interface Props {
|
||||
type: FlowStatusModule['type'];
|
||||
scheduled_for: Date | undefined;
|
||||
skipped?: boolean;
|
||||
}
|
||||
|
||||
let { type, scheduled_for, skipped = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type == 'WaitingForEvents'}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, untrack } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import JobLoader, { type Callbacks } from './JobLoader.svelte'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
@@ -167,8 +167,8 @@
|
||||
testJob = modulesTestStates.states?.[mod.id]?.testJob
|
||||
})
|
||||
|
||||
modulesTestStates.states[mod.id] = {
|
||||
...(modulesTestStates.states?.[mod.id] ?? { loading: false }),
|
||||
modulesTestStates.states[untrack(() => mod).id] = {
|
||||
...(modulesTestStates.states?.[untrack(() => mod).id] ?? { loading: false }),
|
||||
loading: testIsLoading,
|
||||
testJob: testJob
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
import { Button } from './common'
|
||||
import { X, Plus } from 'lucide-svelte'
|
||||
|
||||
export let extra_params: Record<string, string> = {}
|
||||
interface Props {
|
||||
extra_params?: Record<string, string>;
|
||||
}
|
||||
|
||||
let extra_params_vec: [string, string][] = Object.entries(extra_params)
|
||||
let { extra_params = $bindable({}) }: Props = $props();
|
||||
|
||||
let extra_params_vec: [string, string][] = $state(Object.entries(extra_params))
|
||||
|
||||
function sync() {
|
||||
extra_params = Object.fromEntries(extra_params_vec)
|
||||
@@ -13,8 +17,8 @@
|
||||
|
||||
{#each extra_params_vec as o}
|
||||
<div class="flex flex-row max-w-md mb-2 gap-2">
|
||||
<input type="text" on:keyup={sync} bind:value={o[0]} />
|
||||
<input type="text" on:keyup={sync} bind:value={o[1]} />
|
||||
<input type="text" onkeyup={sync} bind:value={o[0]} />
|
||||
<input type="text" onkeyup={sync} bind:value={o[1]} />
|
||||
<Button
|
||||
variant="subtle"
|
||||
destructive
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
import { Button } from './common'
|
||||
import { Minus, Plus } from 'lucide-svelte'
|
||||
|
||||
export let scopes: string[] = []
|
||||
interface Props {
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
let { scopes = $bindable([]) }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if scopes && Array.isArray(scopes)}
|
||||
{#each scopes as v}
|
||||
{#each scopes as v, i}
|
||||
<div class="flex flex-row max-w-md mb-2">
|
||||
<input type="text" bind:value={v} />
|
||||
<input type="text" bind:value={scopes[i]} />
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
|
||||
@@ -119,61 +119,65 @@
|
||||
</script>
|
||||
|
||||
<MeltPopover placement="bottom" on:openChange={(e) => e.detail && loadUsers()}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<UserCog class="w-4 h-4 {selected ? 'text-green-500' : 'text-yellow-500'}" />
|
||||
{#if selectedDisplayName}
|
||||
<span class="text-xs truncate max-w-24">{selectedDisplayName}</span>
|
||||
{#snippet trigger()}
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<UserCog class="w-4 h-4 {selected ? 'text-green-500' : 'text-yellow-500'}" />
|
||||
{#if selectedDisplayName}
|
||||
<span class="text-xs truncate max-w-24">{selectedDisplayName}</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{/snippet}
|
||||
{#snippet content({ close: closePopover })}
|
||||
<div class="p-3 flex flex-col gap-2 min-w-48">
|
||||
<div class="text-xs font-medium text-secondary mb-1">{label}</div>
|
||||
<!-- Target option -->
|
||||
{#if targetEmail}
|
||||
<button
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''}"
|
||||
disabled={!canPreserve}
|
||||
onclick={() => onSelect('target')}
|
||||
>
|
||||
<Check class="w-3 h-3 {selected === 'target' ? 'opacity-100' : 'opacity-0'}" />
|
||||
<span class="truncate max-w-40">{targetUsername}</span>
|
||||
<span class="text-xs text-tertiary">{isDeployment ? '(target)' : '(current)'}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<div slot="content" let:close={closePopover} class="p-3 flex flex-col gap-2 min-w-48">
|
||||
<div class="text-xs font-medium text-secondary mb-1">{label}</div>
|
||||
<!-- Target option -->
|
||||
{#if targetEmail}
|
||||
<!-- Me option -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover"
|
||||
onclick={() => onSelect('me')}
|
||||
>
|
||||
<Check class="w-3 h-3 {selected === 'me' ? 'opacity-100' : 'opacity-0'}" />
|
||||
<span class="truncate max-w-40">{$userStore?.username}</span>
|
||||
<span class="text-xs text-tertiary">(me)</span>
|
||||
</button>
|
||||
<!-- Custom / Pick from workspace -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''}"
|
||||
disabled={!canPreserve}
|
||||
onclick={() => onSelect('target')}
|
||||
onclick={() => {
|
||||
closePopover()
|
||||
openModal()
|
||||
}}
|
||||
>
|
||||
<Check class="w-3 h-3 {selected === 'target' ? 'opacity-100' : 'opacity-0'}" />
|
||||
<span class="truncate max-w-40">{targetUsername}</span>
|
||||
<span class="text-xs text-tertiary">{isDeployment ? '(target)' : '(current)'}</span>
|
||||
{#if selected === 'custom' && customUsername}
|
||||
<Check class="w-3 h-3 opacity-100" />
|
||||
<span class="truncate max-w-40">{customUsername}</span>
|
||||
<span class="text-xs text-tertiary">(custom)</span>
|
||||
{:else}
|
||||
<Check class="w-3 h-3 opacity-0" />
|
||||
<Users class="w-3 h-3 text-tertiary" />
|
||||
<span>Pick from workspace…</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<!-- Me option -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover"
|
||||
onclick={() => onSelect('me')}
|
||||
>
|
||||
<Check class="w-3 h-3 {selected === 'me' ? 'opacity-100' : 'opacity-0'}" />
|
||||
<span class="truncate max-w-40">{$userStore?.username}</span>
|
||||
<span class="text-xs text-tertiary">(me)</span>
|
||||
</button>
|
||||
<!-- Custom / Pick from workspace -->
|
||||
<button
|
||||
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: ''}"
|
||||
disabled={!canPreserve}
|
||||
onclick={() => {
|
||||
closePopover()
|
||||
openModal()
|
||||
}}
|
||||
>
|
||||
{#if selected === 'custom' && customUsername}
|
||||
<Check class="w-3 h-3 opacity-100" />
|
||||
<span class="truncate max-w-40">{customUsername}</span>
|
||||
<span class="text-xs text-tertiary">(custom)</span>
|
||||
{:else}
|
||||
<Check class="w-3 h-3 opacity-0" />
|
||||
<Users class="w-3 h-3 text-tertiary" />
|
||||
<span>Pick from workspace…</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</MeltPopover>
|
||||
|
||||
<!-- User selection modal -->
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let title: string
|
||||
export let tooltip: string = ''
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let primary: boolean = true
|
||||
export let childrenWrapperDivClasses: string = ''
|
||||
interface Props {
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
documentationLink?: string | undefined;
|
||||
primary?: boolean;
|
||||
childrenWrapperDivClasses?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
tooltip = '',
|
||||
documentationLink = undefined,
|
||||
primary = true,
|
||||
childrenWrapperDivClasses = '',
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row flex-wrap justify-between items-center pb-2 my-4 mr-2 min-h-16">
|
||||
@@ -31,9 +43,9 @@
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if $$slots.default}
|
||||
{#if children}
|
||||
<div class="my-2 {childrenWrapperDivClasses}">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import { type GridApi, createGrid, type IDatasource } from 'ag-grid-community'
|
||||
|
||||
import 'ag-grid-community/styles/ag-grid.css'
|
||||
@@ -14,15 +16,19 @@
|
||||
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
|
||||
|
||||
let selectedRowIndex = -1
|
||||
export let s3resource: string
|
||||
export let storage: string | undefined
|
||||
export let workspaceId: string | undefined
|
||||
export let disable_download: boolean = false
|
||||
interface Props {
|
||||
s3resource: string
|
||||
storage: string | undefined
|
||||
workspaceId: string | undefined
|
||||
disable_download?: boolean
|
||||
}
|
||||
|
||||
let { s3resource, storage, workspaceId, disable_download = false }: Props = $props()
|
||||
|
||||
let lastSearch: string | undefined = undefined
|
||||
|
||||
let nbRows: number | undefined = undefined
|
||||
let csvSeparatorChar: string = ','
|
||||
let nbRows: number | undefined = $state(undefined)
|
||||
let csvSeparatorChar: string = $state(',')
|
||||
let datasource: IDatasource = {
|
||||
rowCount: 0,
|
||||
getRows: async function (params) {
|
||||
@@ -95,11 +101,9 @@
|
||||
toggleRow(rows[0])
|
||||
}
|
||||
|
||||
let eGui: HTMLDivElement
|
||||
let eGui: HTMLDivElement | undefined = $state()
|
||||
|
||||
$: eGui && mountGrid()
|
||||
|
||||
let error: string | undefined = undefined
|
||||
let error: string | undefined = $state(undefined)
|
||||
async function mountGrid() {
|
||||
if (eGui) {
|
||||
try {
|
||||
@@ -170,7 +174,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
let darkMode: boolean = false
|
||||
let darkMode: boolean = $state(false)
|
||||
run(() => {
|
||||
eGui && mountGrid()
|
||||
})
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
@@ -182,7 +189,7 @@
|
||||
<label for="csvSeparatorChar" class="text-2xs text-secondary">Separator</label>
|
||||
|
||||
<div class="w-12 ml-2 mr-2">
|
||||
<select class="h-8" bind:value={csvSeparatorChar} on:change={(e) => mountGrid()}>
|
||||
<select class="h-8" bind:value={csvSeparatorChar} onchange={(e) => mountGrid()}>
|
||||
<option value=",">,</option>
|
||||
<option value=";">;</option>
|
||||
<option value="\t">\t</option>
|
||||
|
||||
@@ -79,12 +79,14 @@
|
||||
<p class="text-primary text-sm">No permission changes recorded yet</p>
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Changed By</th>
|
||||
<th>Change Type</th>
|
||||
<th>Affected</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{#snippet headerRow()}
|
||||
<tr >
|
||||
<th>Changed By</th>
|
||||
<th>Change Type</th>
|
||||
<th>Affected</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{/snippet}
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each history as change}
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
import { Hourglass, Loader2, Play, RefreshCw } from 'lucide-svelte'
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
let drawer: Drawer
|
||||
let drawer: Drawer | undefined = $state()
|
||||
|
||||
let script: Script
|
||||
let loadQueuedJobs = true
|
||||
let queuedJobsLoading = false
|
||||
let script: Script | undefined = $state()
|
||||
let loadQueuedJobs = $state(true)
|
||||
let queuedJobsLoading = $state(false)
|
||||
let queuedJobs: {
|
||||
status: 'running' | 'queued'
|
||||
jobId: string
|
||||
scheduledFor: string
|
||||
scriptHash: string
|
||||
}[] = []
|
||||
}[] = $state([])
|
||||
|
||||
let cancellingInProgress = false
|
||||
let cancellingInProgress = $state(false)
|
||||
|
||||
async function continuouslyLoadQueuedJobs() {
|
||||
while (loadQueuedJobs) {
|
||||
@@ -40,7 +40,7 @@
|
||||
let qjs = await JobService.listQueue({
|
||||
workspace: $workspaceStore ?? '',
|
||||
orderDesc: false,
|
||||
scriptPathExact: script.path
|
||||
scriptPathExact: script?.path
|
||||
})
|
||||
let loadingQueuedJobs: {
|
||||
status: 'running' | 'queued'
|
||||
@@ -71,12 +71,12 @@
|
||||
cancellingInProgress = true
|
||||
await JobService.cancelPersistentQueuedJobs({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
path: script?.path ?? '',
|
||||
requestBody: {
|
||||
reason: undefined
|
||||
}
|
||||
})
|
||||
sendUserToast(`All jobs cancelled for ${script.path}`)
|
||||
sendUserToast(`All jobs cancelled for ${script?.path}`)
|
||||
cancellingInProgress = false
|
||||
}
|
||||
|
||||
@@ -88,12 +88,12 @@
|
||||
script = persistentScript!
|
||||
loadQueuedJobs = true
|
||||
continuouslyLoadQueuedJobs()
|
||||
drawer.openDrawer?.()
|
||||
drawer?.openDrawer?.()
|
||||
}
|
||||
|
||||
async function exit() {
|
||||
loadQueuedJobs = false
|
||||
drawer.closeDrawer?.()
|
||||
drawer?.closeDrawer?.()
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -117,51 +117,57 @@
|
||||
>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<h2>
|
||||
Queued jobs for {script.path}
|
||||
Queued jobs for {script?.path}
|
||||
</h2>
|
||||
<Button size="md" btnClasses="w-full h-8" variant="default" on:click={loadQueuedJobsOnce}>
|
||||
<RefreshCw class={queuedJobsLoading ? 'animate-spin' : ''} size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th class="text-xs">Script Hash</th>
|
||||
<th class="text-xs">Job ID</th>
|
||||
<th class="text-xs">Status</th>
|
||||
<th class="text-xs">Scheduled For</th>
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each queuedJobs as { jobId, status, scriptHash, scheduledFor }}
|
||||
<tr class="">
|
||||
<td class="text-xs">
|
||||
<a
|
||||
class="pr-3"
|
||||
href="{base}/scripts/get/{scriptHash}?workspace={$workspaceStore}"
|
||||
target="_blank"
|
||||
>
|
||||
{scriptHash}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<a class="pr-3" href="{base}/run/{jobId}?workspace={$workspaceStore}" target="_blank"
|
||||
>{jobId.substring(24)}</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
{#if status === 'running'}
|
||||
<Badge color="yellow" baseClass="!px-1.5">
|
||||
<Play size={14} />
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge baseClass="!px-1.5">
|
||||
<Hourglass size={14} />
|
||||
</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-xs">{scheduledFor}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{#snippet headerRow()}
|
||||
<tr>
|
||||
<th class="text-xs">Script Hash</th>
|
||||
<th class="text-xs">Job ID</th>
|
||||
<th class="text-xs">Status</th>
|
||||
<th class="text-xs">Scheduled For</th>
|
||||
</tr>
|
||||
{/snippet}
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each queuedJobs as { jobId, status, scriptHash, scheduledFor }}
|
||||
<tr class="">
|
||||
<td class="text-xs">
|
||||
<a
|
||||
class="pr-3"
|
||||
href="{base}/scripts/get/{scriptHash}?workspace={$workspaceStore}"
|
||||
target="_blank"
|
||||
>
|
||||
{scriptHash}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
<a
|
||||
class="pr-3"
|
||||
href="{base}/run/{jobId}?workspace={$workspaceStore}"
|
||||
target="_blank">{jobId.substring(24)}</a
|
||||
>
|
||||
</td>
|
||||
<td class="text-xs">
|
||||
{#if status === 'running'}
|
||||
<Badge color="yellow" baseClass="!px-1.5">
|
||||
<Play size={14} />
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge baseClass="!px-1.5">
|
||||
<Hourglass size={14} />
|
||||
</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-xs">{scheduledFor}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/snippet}
|
||||
</TableCustom>
|
||||
|
||||
{#snippet actions()}
|
||||
|
||||
@@ -43,10 +43,10 @@
|
||||
onClick
|
||||
}: Props = $props()
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions({ placement })
|
||||
const [popperRef, popperContent] = createPopperActions({ placement: untrack(() => placement) })
|
||||
|
||||
const popperOptions: PopperOptions<{}> = {
|
||||
placement,
|
||||
placement: untrack(() => placement),
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user