merge: sync with origin/main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-03-04 11:55:50 +00:00
co-authored by Claude Opus 4.6
138 changed files with 4612 additions and 761 deletions
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## [1.649.0](https://github.com/windmill-labs/windmill/compare/v1.648.0...v1.649.0) (2026-03-03)
### Features
* **frontend:** add script recorder for offline replay ([#8200](https://github.com/windmill-labs/windmill/issues/8200)) ([c97d8b4](https://github.com/windmill-labs/windmill/commit/c97d8b4715f86ea83ab2c0223ba859ced690829a))
* move index management out of /srch/, add storage size reporting ([#8169](https://github.com/windmill-labs/windmill/issues/8169)) ([ee01acd](https://github.com/windmill-labs/windmill/commit/ee01acd9a6a2cd68a3f226988bfb46f6a6e64c08))
### Bug Fixes
* clean up slow-load toast interval on component destroy ([#8207](https://github.com/windmill-labs/windmill/issues/8207)) ([26f4f2b](https://github.com/windmill-labs/windmill/commit/26f4f2b399b828185b553289d6560e12261030a3))
* **frontend:** prevent subflow expansion from hiding all insertion points ([#8203](https://github.com/windmill-labs/windmill/issues/8203)) ([e97da86](https://github.com/windmill-labs/windmill/commit/e97da860672171e33054a77d71f4824bb09e540d))
* gracefully handle malformed OAuth entries in instance config ([#8205](https://github.com/windmill-labs/windmill/issues/8205)) ([cac4bdd](https://github.com/windmill-labs/windmill/commit/cac4bdd54f0c3ea80844ac31f7597f418ff7d8ae))
* skip stop_after_if evaluation for skipped (identity) flow steps ([#8201](https://github.com/windmill-labs/windmill/issues/8201)) ([e6f7775](https://github.com/windmill-labs/windmill/commit/e6f7775d4d9a052aefc37260c6ed161146841cd7))
* use exact matching for python requirements directive parsing ([#8199](https://github.com/windmill-labs/windmill/issues/8199)) ([2b2be38](https://github.com/windmill-labs/windmill/commit/2b2be38f129bbe58b6bb3815c4bd94aa03a3da90))
### Performance Improvements
* use two-step query in input history to leverage v2_job index ([#8197](https://github.com/windmill-labs/windmill/issues/8197)) ([50defdd](https://github.com/windmill-labs/windmill/commit/50defdded113b4d2cf0991b3fb642d1cd9a462b7))
## [1.648.0](https://github.com/windmill-labs/windmill/compare/v1.647.2...v1.648.0) (2026-03-02)
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email FROM token WHERE token = $1 AND (expiration > NOW() OR expiration IS NULL)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "19a7ebb2e7e8e57b6e7c974da8eb7c6841a5c4ff12ba7c12c73d691c49dd99ed"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
+89 -83
View File
@@ -860,9 +860,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.16.0"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -870,9 +870,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.37.1"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
dependencies = [
"cc",
"cmake",
@@ -1334,9 +1334,9 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.60.14"
version = "0.60.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b53543b4b86ed43f051644f704a98c7291b3618b67adf057ee77a366fa52fcaa"
checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3"
dependencies = [
"xmlparser",
]
@@ -6173,20 +6173,20 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
@@ -7421,9 +7421,9 @@ dependencies = [
[[package]]
name = "ipnet"
version = "2.11.0"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "ipnetwork"
@@ -10723,6 +10723,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radium"
version = "0.7.0"
@@ -13850,7 +13856,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
dependencies = [
"fastrand",
"getrandom 0.4.1",
"getrandom 0.4.2",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
@@ -15732,7 +15738,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15796,7 +15802,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15809,7 +15815,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"argon2",
@@ -15947,7 +15953,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15970,7 +15976,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15983,7 +15989,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16009,7 +16015,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16019,7 +16025,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16036,7 +16042,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16059,7 +16065,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16082,7 +16088,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16098,7 +16104,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16118,7 +16124,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16138,7 +16144,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16152,7 +16158,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16179,7 +16185,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16204,7 +16210,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16222,7 +16228,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16243,7 +16249,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16263,7 +16269,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16293,7 +16299,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16320,7 +16326,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"lazy_static",
"serde",
@@ -16332,7 +16338,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16355,7 +16361,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16369,7 +16375,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16399,7 +16405,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16413,7 +16419,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16432,7 +16438,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16531,7 +16537,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16550,7 +16556,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"regex",
"serde",
@@ -16565,7 +16571,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16589,7 +16595,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"futures",
@@ -16606,7 +16612,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16622,7 +16628,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16643,7 +16649,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16674,7 +16680,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16698,7 +16704,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16732,7 +16738,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"futures",
@@ -16750,7 +16756,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16759,7 +16765,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16771,7 +16777,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16783,7 +16789,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16795,7 +16801,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16807,7 +16813,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16819,7 +16825,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16830,7 +16836,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16841,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16854,7 +16860,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16878,7 +16884,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16892,7 +16898,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16909,7 +16915,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16924,7 +16930,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16943,7 +16949,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"serde",
@@ -16954,7 +16960,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16991,7 +16997,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"const_format",
@@ -17029,7 +17035,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17040,7 +17046,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17069,7 +17075,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17092,7 +17098,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17125,7 +17131,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17145,7 +17151,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17179,7 +17185,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17220,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17237,7 +17243,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17261,7 +17267,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17285,7 +17291,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17320,7 +17326,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17348,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17371,7 +17377,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17389,7 +17395,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.648.0"
version = "1.649.0"
dependencies = [
"anyhow",
"async-once-cell",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.648.0"
version = "1.649.0"
authors.workspace = true
edition.workspace = true
@@ -76,7 +76,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.648.0"
version = "1.649.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1 +1 @@
9b3339730eb4bb0b564c7c56ac546f33fb3d8905
6fd5a2ce908235a17975ad4dbdf0051cd89334f3
+30 -30
View File
@@ -62,7 +62,7 @@ use windmill_common::{
},
worker::{
is_native_mode_from_env, reload_custom_tags_setting, Connection, HUB_CACHE_DIR,
HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
HUB_RT_CACHE_DIR, NATIVE_MODE_RESOLVED, TMP_LOGS_DIR, WINDMILL_DIR, WORKER_GROUP,
},
KillpillSender, DEFAULT_HUB_BASE_URL, METRICS_ENABLED,
};
@@ -238,8 +238,8 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
)
})?;
create_dir_all(HUB_CACHE_DIR)?;
create_dir_all(BUN_BUNDLE_CACHE_DIR)?;
create_dir_all(&*HUB_CACHE_DIR)?;
create_dir_all(&*BUN_BUNDLE_CACHE_DIR)?;
for path in paths.values() {
tracing::info!("Caching hub script at {path}");
@@ -249,7 +249,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
.as_ref()
.is_some_and(|x| x == &ScriptLang::Deno)
{
let job_dir = format!("{}/cache_init/{}", TMP_DIR, Uuid::new_v4());
let job_dir = format!("{}/cache_init/{}", *WINDMILL_DIR, Uuid::new_v4());
create_dir_all(&job_dir)?;
let _ = windmill_worker::generate_deno_lock(
&Uuid::nil(),
@@ -267,7 +267,7 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
tokio::fs::remove_dir_all(job_dir).await?;
} else if res.language.as_ref().is_some_and(|x| x == &ScriptLang::Bun) {
let job_id = Uuid::new_v4();
let job_dir = format!("{}/cache_init/{}", TMP_DIR, job_id);
let job_dir = format!("{}/cache_init/{}", *WINDMILL_DIR, job_id);
create_dir_all(&job_dir)?;
if let Some(lock) = res.lockfile {
let _ = windmill_worker::prepare_job_dir(&lock, &job_dir).await?;
@@ -384,9 +384,9 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
println!("Fetched {} resource types from hub", resource_types.len());
create_dir_all(HUB_RT_CACHE_DIR)?;
create_dir_all(&*HUB_RT_CACHE_DIR)?;
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
let cache_path = format!("{}/{}", *HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
let content = serde_json::to_string_pretty(&resource_types)
.with_context(|| "Failed to serialize resource types")?;
@@ -398,7 +398,7 @@ async fn cache_hub_resource_types() -> anyhow::Result<()> {
}
pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyhow::Result<()> {
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
let cache_path = format!("{}/{}", *HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
if tokio::fs::metadata(&cache_path).await.is_err() {
tracing::info!(
@@ -969,7 +969,7 @@ Windmill Community Edition {GIT_VERSION}
DirBuilder::new()
.recursive(true)
.create("/tmp/windmill")
.create(&*WINDMILL_DIR)
.expect("could not create initial server dir");
#[cfg(feature = "tantivy")]
@@ -1794,27 +1794,27 @@ pub async fn run_workers(
let mut handles = Vec::with_capacity(num_workers as usize);
for x in [
TMP_LOGS_DIR,
UV_CACHE_DIR,
DENO_CACHE_DIR,
DENO_CACHE_DIR_DEPS,
DENO_CACHE_DIR_NPM,
BUN_CACHE_DIR,
PY310_CACHE_DIR,
PY311_CACHE_DIR,
PY312_CACHE_DIR,
PY313_CACHE_DIR,
BUN_BUNDLE_CACHE_DIR,
GO_CACHE_DIR,
GO_BIN_CACHE_DIR,
RUST_CACHE_DIR,
CSHARP_CACHE_DIR,
NU_CACHE_DIR,
HUB_CACHE_DIR,
POWERSHELL_CACHE_DIR,
JAVA_CACHE_DIR,
RUBY_CACHE_DIR,
TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
&*TMP_LOGS_DIR,
&*UV_CACHE_DIR,
&*DENO_CACHE_DIR,
&*DENO_CACHE_DIR_DEPS,
&*DENO_CACHE_DIR_NPM,
&*BUN_CACHE_DIR,
&*PY310_CACHE_DIR,
&*PY311_CACHE_DIR,
&*PY312_CACHE_DIR,
&*PY313_CACHE_DIR,
&*BUN_BUNDLE_CACHE_DIR,
&*GO_CACHE_DIR,
&*GO_BIN_CACHE_DIR,
&*RUST_CACHE_DIR,
&*CSHARP_CACHE_DIR,
&*NU_CACHE_DIR,
&*HUB_CACHE_DIR,
&*POWERSHELL_CACHE_DIR,
&*JAVA_CACHE_DIR,
&*RUBY_CACHE_DIR,
&*TAR_JAVA_CACHE_DIR, // for related places search: ADD_NEW_LANG
] {
DirBuilder::new()
.recursive(true)
+9 -8
View File
@@ -73,7 +73,7 @@ use windmill_common::{
load_periodic_bash_script_interval_from_env, load_whitelist_env_vars_from_env,
load_worker_config, reload_custom_tags_setting, store_pull_query,
store_suspended_pull_query, Connection, WorkerConfig, DEFAULT_TAGS_PER_WORKSPACE,
DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, TMP_DIR,
DEFAULT_TAGS_WORKSPACES, INDEXER_CONFIG, SCRIPT_TOKEN_EXPIRY, SMTP_CONFIG, WINDMILL_DIR,
WORKER_CONFIG, WORKER_GROUP,
},
KillpillSender, BASE_URL, CRITICAL_ALERTS_ON_DB_OVERSIZE, CRITICAL_ALERT_MUTE_UI_ENABLED,
@@ -595,7 +595,7 @@ async fn sleep_until_next_minute_start_plus_one_s() {
use windmill_common::tracing_init::TMP_WINDMILL_LOGS_SERVICE;
async fn find_two_highest_files(hostname: &str) -> (Option<String>, Option<String>) {
let log_dir = format!("{}/{}/", TMP_WINDMILL_LOGS_SERVICE, hostname);
let log_dir = format!("{}/{}/", *TMP_WINDMILL_LOGS_SERVICE, hostname);
let rd_dir = tokio::fs::read_dir(log_dir).await;
if let Ok(mut log_files) = rd_dir {
let mut highest_file: Option<String> = None;
@@ -614,7 +614,8 @@ async fn find_two_highest_files(hostname: &str) -> (Option<String>, Option<Strin
(highest_file, second_highest_file)
} else {
tracing::error!(
"Error reading log files: {TMP_WINDMILL_LOGS_SERVICE}, {:#?}",
"Error reading log files: {}, {:#?}",
*TMP_WINDMILL_LOGS_SERVICE,
rd_dir.unwrap_err()
);
(None, None)
@@ -716,7 +717,7 @@ async fn send_log_file_to_object_store(
let s3_client = windmill_object_store::get_object_store().await;
#[cfg(feature = "parquet")]
if let Some(s3_client) = s3_client {
let path = std::path::Path::new(TMP_WINDMILL_LOGS_SERVICE)
let path = std::path::Path::new(&*TMP_WINDMILL_LOGS_SERVICE)
.join(hostname)
.join(&highest_file);
@@ -935,7 +936,7 @@ pub async fn delete_expired_items(db: &DB) -> () {
.iter()
.map(|f| format!("{}/{}", f.hostname, f.file_path))
.collect();
delete_log_files_from_disk_and_store(paths, TMP_WINDMILL_LOGS_SERVICE, windmill_common::tracing_init::LOGS_SERVICE).await;
delete_log_files_from_disk_and_store(paths, &*TMP_WINDMILL_LOGS_SERVICE, windmill_common::tracing_init::LOGS_SERVICE).await;
}
Err(e) => tracing::error!("Error deleting log file: {:?}", e),
@@ -1140,7 +1141,7 @@ async fn delete_expired_jobs_batch(
.filter_map(|opt| opt)
.flat_map(|inner_vec| inner_vec.into_iter())
.collect();
delete_log_files_from_disk_and_store(paths, TMP_DIR, "").await;
delete_log_files_from_disk_and_store(paths, &*WINDMILL_DIR, "").await;
}
Err(e) => tracing::error!("Error deleting job logs: {:?}", e),
}
@@ -1367,7 +1368,7 @@ pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
let settings_xml = MAVEN_SETTINGS_XML.read().await.clone();
match settings_xml {
Some(ref content) if !content.trim().is_empty() => {
let m2_dir = format!("{JAVA_HOME_DIR}/.m2");
let m2_dir = format!("{}/.m2", *JAVA_HOME_DIR);
if let Err(e) = tokio::fs::create_dir_all(&m2_dir).await {
tracing::error!("Failed to create .m2 directory: {e:#}");
return;
@@ -1378,7 +1379,7 @@ pub async fn reload_maven_settings_xml_setting(conn: &Connection) {
}
}
_ => {
let settings_path = format!("{JAVA_HOME_DIR}/.m2/settings.xml");
let settings_path = format!("{}/.m2/settings.xml", *JAVA_HOME_DIR);
let _ = tokio::fs::remove_file(&settings_path).await;
}
}
+323
View File
@@ -0,0 +1,323 @@
//! Tests for WM_END_USER_EMAIL environment variable.
//!
//! These tests verify that WM_END_USER_EMAIL is populated with the authenticated
//! user's email when executing app components.
//!
//! TODO: Add tests for scripts and flows once public execution endpoints are identified.
//! Currently only apps support non-workspace-member execution via OptAuthed + token lookup.
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_common::worker::Connection;
use windmill_test_utils::*;
const SAME_WS_TOKEN: &str = "SECRET_TOKEN";
const OTHER_WS_TOKEN: &str = "OTHER_WS_TOKEN";
const NO_WS_TOKEN: &str = "NO_WS_TOKEN";
const SAME_WS_EMAIL: &str = "test@windmill.dev";
const OTHER_WS_EMAIL: &str = "other-ws@windmill.dev";
const NO_WS_EMAIL: &str = "no-ws@windmill.dev";
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
builder.header("Authorization", format!("Bearer {}", token))
}
// TODO: Script tests - need to identify public execution endpoints for non-workspace-members
// async fn run_script(port: u16, token: &str) -> anyhow::Result<String> {
// let url = format!(
// "http://localhost:{}/api/w/test-workspace/jobs/run_wait_result/p/f/test/get_end_user_email",
// port
// );
// let resp = authed(client().post(&url), token)
// .json(&json!({}))
// .send()
// .await?;
// if !resp.status().is_success() {
// anyhow::bail!("script run failed: {} - {}", resp.status(), resp.text().await?);
// }
// Ok(resp.json::<serde_json::Value>().await?
// .as_str().unwrap_or("").to_string())
// }
// TODO: Flow tests - need to identify public execution endpoints for non-workspace-members
// async fn run_flow(port: u16, token: &str) -> anyhow::Result<String> {
// let url = format!(
// "http://localhost:{}/api/w/test-workspace/jobs/run_wait_result/f/f/test/get_end_user_email_flow",
// port
// );
// let resp = authed(client().post(&url), token)
// .json(&json!({}))
// .send()
// .await?;
// if !resp.status().is_success() {
// anyhow::bail!("flow run failed: {} - {}", resp.status(), resp.text().await?);
// }
// Ok(resp.json::<serde_json::Value>().await?
// .as_str().unwrap_or("").to_string())
// }
/// Create an app with inline script via API
async fn create_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<()> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps/create",
port
);
let resp = authed(client().post(&url), SAME_WS_TOKEN)
.json(&json!({
"path": path,
"summary": "Test app for WM_END_USER_EMAIL",
"value": {
"type": "app",
"grid": [],
"subgrids": {},
"hiddenInlineScripts": [{
"name": "get_email",
"language": "deno",
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }",
"path": "f/test/email_app/get_email"
}]
},
"policy": {
"execution_mode": "anonymous",
"on_behalf_of": null,
"on_behalf_of_email": null,
"triggerables_v2": {
"get_email": {
"static_inputs": {},
"one_of_inputs": {}
},
// SHA256 hash of raw_code content for anonymous execution
"rawscript/6428aba5aa2d3ea8e1215bfdccbedd3718b18da7a239e3778a9787bb9a0ea606": {
"static_inputs": {},
"one_of_inputs": {}
}
}
}
}))
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("create app failed: {} - {}", resp.status(), resp.text().await?);
}
Ok(())
}
/// Create a raw app with inline script via API (uses regular app endpoint with rawapp type)
async fn create_raw_app_with_inline_script(port: u16, path: &str) -> anyhow::Result<()> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps/create",
port
);
let resp = authed(client().post(&url), SAME_WS_TOKEN)
.json(&json!({
"path": path,
"summary": "Test raw app for WM_END_USER_EMAIL",
"value": {
"type": "rawapp",
"css": "",
"inlineScripts": [{
"name": "get_email",
"language": "deno",
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }"
}]
},
"policy": {
"execution_mode": "anonymous",
"on_behalf_of": null,
"on_behalf_of_email": null,
"triggerables_v2": {
"get_email": {
"static_inputs": {},
"one_of_inputs": {}
},
// SHA256 hash of raw_code content for anonymous execution
"rawscript/6428aba5aa2d3ea8e1215bfdccbedd3718b18da7a239e3778a9787bb9a0ea606": {
"static_inputs": {},
"one_of_inputs": {}
}
}
}
}))
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("create raw app failed: {} - {}", resp.status(), resp.text().await?);
}
Ok(())
}
async fn run_app_inline_script(port: u16, token: &str, app_path: &str, force_viewer: bool) -> anyhow::Result<String> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}",
port, app_path
);
let mut payload = json!({
"args": {},
"component": "get_email",
"raw_code": {
"language": "deno",
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }",
"path": format!("{}/get_email", app_path)
}
});
if force_viewer {
payload["force_viewer_static_fields"] = json!({});
}
let resp = authed(client().post(&url), token)
.json(&payload)
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("app inline script run failed: {} - {}", resp.status(), resp.text().await?);
}
let job_id = resp.text().await?;
wait_for_job_result(port, token, &job_id).await
}
async fn run_raw_app_inline_script(port: u16, token: &str, app_path: &str, force_viewer: bool) -> anyhow::Result<String> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/apps_u/execute_component/{}",
port, app_path
);
let mut payload = json!({
"args": {},
"component": "get_email",
"raw_code": {
"language": "deno",
"content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }"
}
});
if force_viewer {
payload["force_viewer_static_fields"] = json!({});
}
let resp = authed(client().post(&url), token)
.json(&payload)
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("raw app inline script run failed: {} - {}", resp.status(), resp.text().await?);
}
let job_id = resp.text().await?;
wait_for_job_result(port, token, &job_id).await
}
async fn wait_for_job_result(port: u16, token: &str, job_id: &str) -> anyhow::Result<String> {
let url = format!(
"http://localhost:{}/api/w/test-workspace/jobs_u/completed/get_result/{}",
port, job_id
);
for _ in 0..100 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let resp = authed(client().get(&url), token).send().await?;
if resp.status().is_success() {
return Ok(resp.json::<serde_json::Value>().await?
.as_str().unwrap_or("").to_string());
}
}
anyhow::bail!("timeout waiting for job result")
}
// TODO: Script tests - need to identify public execution endpoints for non-workspace-members
// #[cfg(feature = "deno_core")]
// #[sqlx::test(fixtures("base", "end_user_email"))]
// async fn test_script_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
// initialize_tracing().await;
// set_jwt_secret().await;
// let server = ApiServer::start(db.clone()).await?;
// let port = server.addr.port();
//
// in_test_worker(Connection::Sql(db.clone()), async move {
// let result = run_script(port, SAME_WS_TOKEN).await?;
// assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
// Ok::<(), anyhow::Error>(())
// }, port).await?;
//
// Ok(())
// }
// TODO: Flow tests - need to identify public execution endpoints for non-workspace-members
// #[cfg(feature = "deno_core")]
// #[sqlx::test(fixtures("base", "end_user_email"))]
// async fn test_flow_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
// initialize_tracing().await;
// set_jwt_secret().await;
// let server = ApiServer::start(db.clone()).await?;
// let port = server.addr.port();
//
// in_test_worker(Connection::Sql(db.clone()), async move {
// let result = run_flow(port, SAME_WS_TOKEN).await?;
// assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
// Ok::<(), anyhow::Error>(())
// }, port).await?;
//
// Ok(())
// }
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "end_user_email"))]
async fn test_app_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
set_jwt_secret().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let app_path = "f/test/email_app";
in_test_worker(Connection::Sql(db.clone()), async move {
// Create the app with inline script first
create_app_with_inline_script(port, app_path).await?;
// Same workspace user (force_viewer mode works for workspace members)
let result = run_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
// Other workspace user (uses app's anonymous policy + token lookup)
let result = run_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
assert_eq!(result, OTHER_WS_EMAIL, "other workspace user should get their email");
// No workspace user (uses app's anonymous policy + token lookup)
let result = run_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
assert_eq!(result, NO_WS_EMAIL, "no workspace user should get their email");
Ok::<(), anyhow::Error>(())
}, port).await?;
Ok(())
}
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base", "end_user_email"))]
async fn test_raw_app_wm_end_user_email(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
set_jwt_secret().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let app_path = "f/test/email_raw_app";
in_test_worker(Connection::Sql(db.clone()), async move {
// Create the raw app with inline script first
create_raw_app_with_inline_script(port, app_path).await?;
// Same workspace user (force_viewer mode works for workspace members)
let result = run_raw_app_inline_script(port, SAME_WS_TOKEN, app_path, true).await?;
assert_eq!(result, SAME_WS_EMAIL, "same workspace user should get their email");
// Other workspace user (uses app's anonymous policy + token lookup)
let result = run_raw_app_inline_script(port, OTHER_WS_TOKEN, app_path, false).await?;
assert_eq!(result, OTHER_WS_EMAIL, "other workspace user should get their email");
// No workspace user (uses app's anonymous policy + token lookup)
let result = run_raw_app_inline_script(port, NO_WS_TOKEN, app_path, false).await?;
assert_eq!(result, NO_WS_EMAIL, "no workspace user should get their email");
Ok::<(), anyhow::Error>(())
}, port).await?;
Ok(())
}
+63
View File
@@ -0,0 +1,63 @@
-- Fixture for WM_END_USER_EMAIL tests
-- Sets up 3 users with different workspace memberships:
-- 1. test@windmill.dev - in test-workspace (from base.sql)
-- 2. other-ws@windmill.dev - in other-workspace only
-- 3. no-ws@windmill.dev - not in any workspace
-- Second workspace for cross-workspace user
INSERT INTO workspace (id, name, owner)
VALUES ('other-workspace', 'other-workspace', 'other-ws-user');
INSERT INTO workspace_key(workspace_id, kind, key)
VALUES ('other-workspace', 'cloud', 'other-key');
INSERT INTO workspace_settings (workspace_id)
VALUES ('other-workspace');
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('other-workspace', 'all', 'All users', '{}');
-- User in other-workspace only (not in test-workspace)
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('other-ws@windmill.dev', 'hash', 'password', false, true, 'Other WS User');
INSERT INTO usr(workspace_id, email, username, is_admin, role)
VALUES ('other-workspace', 'other-ws@windmill.dev', 'other-ws-user', true, 'Admin');
INSERT INTO token(token, email, label, super_admin)
VALUES ('OTHER_WS_TOKEN', 'other-ws@windmill.dev', 'other ws token', false);
-- User not in any workspace
INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('no-ws@windmill.dev', 'hash', 'password', false, true, 'No WS User');
INSERT INTO token(token, email, label, super_admin)
VALUES ('NO_WS_TOKEN', 'no-ws@windmill.dev', 'no ws token', false);
-- Script that returns WM_END_USER_EMAIL (public via extra_perms)
INSERT INTO script (workspace_id, created_by, content, schema, summary, description, path, hash, language, lock, kind, extra_perms)
VALUES (
'test-workspace', 'test-user',
'export function main() { return Deno.env.get("WM_END_USER_EMAIL") || ""; }',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'Returns WM_END_USER_EMAIL', '', 'f/test/get_end_user_email', 900001, 'deno', '', 'script',
'{"g/all": true}'
);
-- Flow that returns WM_END_USER_EMAIL (public via extra_perms)
INSERT INTO flow (workspace_id, summary, description, path, versions, schema, value, edited_by, extra_perms)
VALUES (
'test-workspace', 'Returns WM_END_USER_EMAIL', '', 'f/test/get_end_user_email_flow', '{900002}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}',
'test-user',
'{"g/all": true}'
);
INSERT INTO flow_version (id, workspace_id, path, schema, value, created_by)
VALUES (
900002, 'test-workspace', 'f/test/get_end_user_email_flow',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'{"modules": [{"id": "a", "value": {"type": "rawscript", "language": "deno", "content": "export function main() { return Deno.env.get(\"WM_END_USER_EMAIL\") || \"\"; }", "input_transforms": {}}}]}',
'test-user'
);
+1 -1
View File
@@ -206,7 +206,7 @@ fn spawn_workers(
std::fs::DirBuilder::new()
.recursive(true)
.create(windmill_worker::GO_BIN_CACHE_DIR)
.create(&*windmill_worker::GO_BIN_CACHE_DIR)
.expect("could not create initial worker dir");
let (tx, _) = KillpillSender::new(n + 1);
+15 -8
View File
@@ -1,7 +1,7 @@
use windmill_test_utils::*;
use sqlx::postgres::Postgres;
use sqlx::Pool;
use windmill_common::scripts::ScriptLang;
use windmill_test_utils::*;
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "lockfile_python"))]
@@ -188,7 +188,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -207,14 +208,14 @@ def main():
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base"))]
async fn test_python_global_site_packages(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::{cache::concatcp, worker::ROOT_CACHE_DIR};
use windmill_common::worker::ROOT_CACHE_DIR;
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Shared for all 3.12.*
let path = concatcp!(ROOT_CACHE_DIR, "python_3_12/global-site-packages").to_owned();
let path = format!("{}python_3_12/global-site-packages", *ROOT_CACHE_DIR);
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path + "/my_global_site_package_3_12_any.py", "").unwrap();
@@ -237,7 +238,9 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -271,7 +274,9 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default(
)
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -310,7 +315,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
@@ -347,7 +353,8 @@ def main():
path: None,
language: ScriptLang::Python3,
lock: None,
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default().into(),
concurrency_settings: windmill_common::runnable_settings::ConcurrencySettings::default()
.into(),
debouncing_settings: windmill_common::runnable_settings::DebouncingSettings::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
+38
View File
@@ -35,7 +35,45 @@ use windmill_common::{
lazy_static::lazy_static! {
// Global auth cache accessible from main.rs for direct invalidation
pub static ref AUTH_CACHE: Cache<(String, String), ExpiringAuthCache> = Cache::new(300);
// Cache for token -> email lookups (for non-workspace-member authenticated users)
static ref TOKEN_EMAIL_CACHE: Cache<String, Option<String>> = Cache::new(500);
}
/// Get email from a valid token, with caching.
/// Used for WM_END_USER_EMAIL when user is authenticated but not a workspace member.
async fn get_email_from_token(db: &DB, token: &str) -> Option<String> {
if let Some(cached) = TOKEN_EMAIL_CACHE.get(token) {
return cached;
}
let email = sqlx::query_scalar!(
"SELECT email FROM token WHERE token = $1 AND (expiration > NOW() OR expiration IS NULL)",
token
)
.fetch_optional(db)
.await
.ok()
.flatten()
.flatten(); // email column is nullable, so we get Option<Option<String>>
TOKEN_EMAIL_CACHE.insert(token.to_string(), email.clone());
email
}
/// Get end user email from authenticated user or token.
/// Returns email if user is authenticated (workspace member) or has valid instance token.
pub async fn get_end_user_email(
db: &DB,
opt_authed: Option<&ApiAuthed>,
token: Option<&str>,
) -> Option<String> {
if let Some(authed) = opt_authed {
return Some(authed.email.clone());
}
if let Some(token) = token {
return get_email_from_token(db, token).await;
}
None
}
// Global function to invalidate a specific token from cache
pub fn invalidate_token_from_cache(token: &str) {
+2 -2
View File
@@ -29,8 +29,8 @@ use scopes::ScopeDefinition;
// Re-export key auth types and functions
pub use auth::{
invalidate_token_from_cache, AuthCache, ExpiringAuthCache, OptTokened, Tokened,
TruncatedTokenWithEmail, AUTH_CACHE,
get_end_user_email, invalidate_token_from_cache, AuthCache, ExpiringAuthCache, OptTokened,
Tokened, TruncatedTokenWithEmail, AUTH_CACHE,
};
// ------------ ApiAuthed & OptJobAuthed types ------------
+1 -1
View File
@@ -1085,7 +1085,7 @@ async fn sync_cached_resource_types(
require_super_admin(&db, &authed.email).await?;
use windmill_common::worker::HUB_RT_CACHE_DIR;
let cache_path = format!("{}/resource_types.json", HUB_RT_CACHE_DIR);
let cache_path = format!("{}/resource_types.json", *HUB_RT_CACHE_DIR);
let content = tokio::fs::read_to_string(&cache_path).await.map_err(|e| {
error::Error::NotFound(format!(
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.648.0
version: 1.649.0
title: Windmill API
contact:
+2 -2
View File
@@ -8,7 +8,7 @@ use std::{collections::HashMap, sync::Arc};
* LICENSE-AGPL for a copy of the license.
*/
use crate::{
auth::OptTokened,
auth::{get_end_user_email, OptTokened},
db::{ApiAuthed, DB},
jobs::RunJobQuery,
users::{require_owner_of_path, OptAuthed},
@@ -2149,7 +2149,7 @@ async fn execute_component(
(email.as_str(), permissioned_as)
};
let end_user_email = opt_authed.as_ref().map(|a| a.email.clone());
let end_user_email = get_end_user_email(&db, opt_authed.as_ref(), tokened.token.as_deref()).await;
let (uuid, mut tx) = push(
&db,
+3 -2
View File
@@ -1,4 +1,5 @@
pub use windmill_api_auth::auth::{
invalidate_token_from_cache, list_tokens_internal, transform_old_scope_to_new_scope, AuthCache,
ExpiringAuthCache, OptTokened, Tokened, TruncatedTokenWithEmail,
get_end_user_email, invalidate_token_from_cache, list_tokens_internal,
transform_old_scope_to_new_scope, AuthCache, ExpiringAuthCache, OptTokened, Tokened,
TruncatedTokenWithEmail,
};
+8 -8
View File
@@ -47,7 +47,7 @@ use windmill_common::runtime_assets::{register_runtime_asset, InsertRuntimeAsset
use windmill_common::scripts::ScriptRunnableSettingsInline;
use windmill_common::triggers::TriggerMetadata;
use windmill_common::utils::{RunnableKind, WarnAfterExt};
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::worker::{Connection, CLOUD_HOSTED, WINDMILL_DIR};
use windmill_common::workspace_dependencies::{
RawWorkspaceDependencies, MIN_VERSION_WORKSPACE_DEPENDENCIES,
};
@@ -1412,7 +1412,7 @@ async fn get_logs_from_disk(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
for file_p in &file_index {
if !tokio::fs::metadata(format!("{TMP_DIR}/{file_p}"))
if !tokio::fs::metadata(format!("{}/{file_p}", *WINDMILL_DIR))
.await
.is_ok()
{
@@ -1427,7 +1427,7 @@ async fn get_logs_from_disk(
"#.to_string(),
));
for file_p in file_index.clone() {
let mut file = tokio::fs::File::open(format!("{TMP_DIR}/{file_p}")).await.map_err(to_anyhow)?;
let mut file = tokio::fs::File::open(format!("{}/{file_p}", *WINDMILL_DIR)).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(to_anyhow)?;
yield Ok(bytes::Bytes::from(buffer)) as anyhow::Result<bytes::Bytes>;
@@ -5888,7 +5888,7 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
));
}
let local_file = format!("{TMP_DIR}/logs/{file_p}");
let local_file = format!("{}/logs/{file_p}", *WINDMILL_DIR);
if tokio::fs::metadata(&local_file).await.is_ok() {
let mut file = tokio::fs::File::open(local_file).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
@@ -5934,10 +5934,10 @@ async fn get_log_file(Path((_w_id, file_p)): Path<(String, String)>) -> error::R
}
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
return Err(error::Error::NotFound(format!(
"File not found on server logs volume /tmp/windmill/logs and no distributed logs s3 storage for {}",
file_p
)));
return Err(error::Error::NotFound(format!(
"File not found on server logs volume {}/logs and no distributed logs s3 storage for {}",
*WINDMILL_DIR, file_p
)));
}
async fn get_job_update(
+1 -1
View File
@@ -262,7 +262,7 @@ pub async fn run_server(
) -> anyhow::Result<()> {
let user_db = UserDB::new(db.clone());
for x in [HUB_CACHE_DIR] {
for x in [&*HUB_CACHE_DIR] {
DirBuilder::new()
.recursive(true)
.create(x)
+6 -2
View File
@@ -102,7 +102,11 @@ async fn get_log_file(
#[cfg(feature = "parquet")]
if let Some(s3_client) = s3_client {
let path = format!("{}{}", windmill_common::tracing_init::LOGS_SERVICE, path);
let file = s3_client.get(&windmill_object_store::object_store_reexports::Path::from(path)).await;
let file = s3_client
.get(&windmill_object_store::object_store_reexports::Path::from(
path,
))
.await;
match file {
Ok(file) => {
let bytes = file.bytes().await;
@@ -126,7 +130,7 @@ async fn get_log_file(
}
}
}
let file = tokio::fs::read(format!("{}{}", TMP_WINDMILL_LOGS_SERVICE, path)).await;
let file = tokio::fs::read(format!("{}{}", *TMP_WINDMILL_LOGS_SERVICE, path)).await;
if let Ok(bytes) = file {
Ok(content_plain(Body::from(bytes::Bytes::from(bytes))))
} else {
@@ -43,6 +43,7 @@ use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings
use windmill_common::scripts::ScriptRunnableSettingsHandle;
use windmill_common::utils::require_admin;
use windmill_common::variables::decrypt;
use windmill_common::worker::WINDMILL_DIR;
use windmill_common::{
db::UserDB,
error::{to_anyhow, Error, Result},
@@ -372,7 +373,7 @@ pub(crate) async fn tarball_workspace(
let mut tx = user_db.begin(&authed).await?;
let tmp_dir = TempDir::new_in("/tmp/windmill/")?;
let tmp_dir = TempDir::new_in(&*WINDMILL_DIR)?;
let name = match archive_type.as_deref() {
Some("tar") | None => Ok(format!("windmill-{w_id}.tar")),
+3 -2
View File
@@ -1,5 +1,5 @@
use crate::{
worker::{write_file, TMP_DIR},
worker::{write_file, WINDMILL_DIR},
DB,
};
use serde::Serialize;
@@ -113,7 +113,8 @@ impl BenchmarkInfo {
"Writing benchmark {path}, duration of benchmark: {total_duration}ms and RPS: {}{pool_info}",
self.iters as f64 / total_duration as f64 * 1000.0
);
write_file(TMP_DIR, path, &serde_json::to_string(&self).unwrap()).expect("write profiling");
write_file(&WINDMILL_DIR, path, &serde_json::to_string(&self).unwrap())
.expect("write profiling");
Ok(())
}
}
+3 -3
View File
@@ -18,7 +18,7 @@ use crate::{
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
users::username_to_permissioned_as,
utils::{StripPath, HTTP_CLIENT},
worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, TMP_DIR},
worker::{to_raw_value, CUSTOM_TAGS_PER_WORKSPACE, WINDMILL_DIR},
FlowVersionInfo, ScriptHashInfo, Tag,
};
@@ -225,7 +225,7 @@ pub async fn get_logs_from_disk(
if log_offset > 0 {
if let Some(file_index) = log_file_index.clone() {
for file_p in &file_index {
if !tokio::fs::metadata(format!("{TMP_DIR}/{file_p}"))
if !tokio::fs::metadata(format!("{}/{file_p}", *WINDMILL_DIR))
.await
.is_ok()
{
@@ -236,7 +236,7 @@ pub async fn get_logs_from_disk(
let logs = logs.to_string();
let stream = async_stream::stream! {
for file_p in file_index.clone() {
let mut file = tokio::fs::File::open(format!("{TMP_DIR}/{file_p}")).await.map_err(to_anyhow)?;
let mut file = tokio::fs::File::open(format!("{}/{file_p}", *WINDMILL_DIR)).await.map_err(to_anyhow)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await.map_err(to_anyhow)?;
yield Ok(bytes::Bytes::from(buffer)) as anyhow::Result<bytes::Bytes>;
+2 -2
View File
@@ -206,12 +206,12 @@ pub async fn get_full_hub_script_by_path(
let version = path_iterator
.next()
.ok_or_else(|| Error::internal_err(format!("expected hub path to have version number")))?;
let cache_path = format!("{HUB_CACHE_DIR}/{version}");
let cache_path = format!("{}/{version}", *HUB_CACHE_DIR);
let script;
if tokio::fs::metadata(&cache_path).await.is_err() {
script = get_full_hub_script_by_path_inner(path, http_client, db).await?;
if let Err(e) = crate::worker::write_file(
HUB_CACHE_DIR,
&HUB_CACHE_DIR,
&version,
&serde_json::to_string(&script).map_err(to_anyhow)?,
) {
+4 -4
View File
@@ -6,8 +6,6 @@
* LICENSE-AGPL for a copy of the license.
*/
use const_format::concatcp;
use std::{
collections::HashMap,
sync::{Arc, RwLock},
@@ -61,7 +59,9 @@ fn create_targets_filter(default_env_filter: LevelFilter) -> Targets {
pub const LOGS_SERVICE: &str = "logs/services/";
pub const TMP_WINDMILL_LOGS_SERVICE: &str = concatcp!("/tmp/windmill/", LOGS_SERVICE);
lazy_static::lazy_static! {
pub static ref TMP_WINDMILL_LOGS_SERVICE: String = format!("{}/{}", *crate::worker::WINDMILL_DIR, LOGS_SERVICE);
}
pub fn initialize_tracing(
hostname: &str,
@@ -108,7 +108,7 @@ pub fn initialize_tracing(
use tracing_appender::rolling::{RollingFileAppender, Rotation};
let log_dir = format!("{}/{}/", TMP_WINDMILL_LOGS_SERVICE, hostname);
let log_dir = format!("{}/{}/", *TMP_WINDMILL_LOGS_SERVICE, hostname);
std::fs::create_dir_all(&log_dir).unwrap();
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::MINUTELY)
+20 -9
View File
@@ -2,7 +2,6 @@
use anyhow::anyhow;
use axum::http::HeaderMap;
use bytes::Bytes;
use const_format::concatcp;
use itertools::Itertools;
use regex::Regex;
use reqwest_middleware::ClientWithMiddleware;
@@ -274,7 +273,9 @@ lazy_static::lazy_static! {
pub static ref ROOT_STANDALONE_BUNDLE_DIR: String = format!("{}/.windmill/standalone_bundle", std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()));
}
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
lazy_static::lazy_static! {
pub static ref ROOT_CACHE_NOMOUNT_DIR: String = format!("{}/cache_nomount/", *WINDMILL_DIR);
}
/// Whether native mode is forced by the environment (NATIVE_MODE=true env var or WORKER_GROUP=native).
/// This does NOT account for native_mode set in the DB worker group config — for that, read
@@ -490,13 +491,23 @@ pub async fn store_pull_query(wc: &WorkerConfig) {
*l = queries;
}
pub const TMP_DIR: &str = "/tmp/windmill";
pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
pub const HUB_RT_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub_rt");
pub const ROOT_CACHE_DIR: &str = concatcp!(TMP_DIR, "/cache/");
lazy_static::lazy_static! {
pub static ref WINDMILL_DIR: String = {
let dir = std::env::var("WINDMILL_DIR")
.unwrap_or_else(|_| "/tmp/windmill".to_string());
if dir.is_empty() {
panic!("WINDMILL_DIR must not be empty");
}
if dir.ends_with('/') || dir.ends_with('\\') {
panic!("WINDMILL_DIR must not end with a trailing slash, got: {dir}");
}
dir
};
pub static ref TMP_LOGS_DIR: String = format!("{}/logs", *WINDMILL_DIR);
pub static ref ROOT_CACHE_DIR: String = format!("{}/cache/", *WINDMILL_DIR);
pub static ref HUB_CACHE_DIR: String = format!("{}hub", *ROOT_CACHE_DIR);
pub static ref HUB_RT_CACHE_DIR: String = format!("{}hub_rt", *ROOT_CACHE_DIR);
}
pub fn write_file(dir: &str, path: &str, content: &str) -> error::Result<File> {
let path = format!("{}/{}", dir, path);
+1
View File
@@ -2164,6 +2164,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"duckdb",
"regex",
"rust_decimal",
"serde",
"serde_json",
@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
chrono = "0.4.41"
duckdb = { version = "1.4.4", features = ["bundled"] }
regex = "1"
rust_decimal = "1.37.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
+219 -36
View File
@@ -1,12 +1,14 @@
use std::{
collections::HashMap,
ffi::{CStr, CString, c_char, c_uint},
ffi::{c_char, c_uint, CStr, CString},
ptr::null_mut,
sync::LazyLock,
};
use duckdb::{Row, core::LogicalTypeId, params_from_iter, types::TimeUnit};
use rust_decimal::{Decimal, prelude::FromPrimitive};
use serde::Deserialize;
use duckdb::{core::LogicalTypeId, params_from_iter, types::TimeUnit, Row};
use regex::Regex;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
#[derive(Deserialize, Clone, Debug, PartialEq, Default)]
@@ -96,6 +98,218 @@ pub extern "C" fn run_duckdb_ffi(
})
}
#[derive(Serialize, Debug)]
struct PrepareQueryColumnInfo {
name: String,
#[serde(rename = "type")]
type_name: String,
}
#[derive(Serialize, Debug)]
struct PrepareQueryResult {
#[serde(skip_serializing_if = "Option::is_none")]
columns: Option<Vec<PrepareQueryColumnInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}
fn is_setup_statement(query: &str) -> bool {
let trimmed = query.trim_start();
let upper = trimmed.to_uppercase();
upper.starts_with("ATTACH")
|| upper.starts_with("USE")
|| upper.starts_with("INSTALL")
|| upper.starts_with("LOAD")
|| upper.starts_with("SET")
|| upper.starts_with("RESET")
|| upper.starts_with("CREATE OR REPLACE SECRET")
|| upper.starts_with("CREATE SECRET")
}
/// Returns true if the query is expected to return a result set and can be wrapped with DESCRIBE.
fn is_describable_query(query: &str) -> bool {
let trimmed = query.trim_start();
let upper = trimmed.to_uppercase();
upper.starts_with("SELECT")
|| upper.starts_with("WITH")
|| upper.starts_with("VALUES")
|| upper.starts_with("TABLE")
|| upper.starts_with("FROM")
}
static PARAM_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\$\d+").expect("invalid regex"));
fn replace_params_with_null(query: &str) -> String {
PARAM_RE.replace_all(query, "NULL").to_string()
}
#[unsafe(no_mangle)]
pub extern "C" fn prepare_duckdb_ffi(
query_block_list: *const *const c_char,
query_block_list_count: usize,
token: *const c_char,
base_internal_url: *const c_char,
w_id: *const c_char,
) -> *mut c_char {
let r = match convert_prepare_args(
query_block_list,
query_block_list_count,
token,
base_internal_url,
w_id,
)
.and_then(|(query_block_list, token, base_internal_url, w_id)| {
prepare_duckdb_internal(query_block_list, token, base_internal_url, w_id)
}) {
Ok(result) => result,
Err(err) => {
let err = serde_json::to_string(&err)
.unwrap_or_else(|_| "Unknown error in duckdb ffi lib".to_string());
format!("ERROR {}", err)
}
};
CString::new(r).map(|s| s.into_raw()).unwrap_or_else(|e| {
println!("Failed to allocate error string in duckdb ffi lib: {:?}", e);
null_mut()
})
}
fn setup_duckdb_connection(
conn: &duckdb::Connection,
token: &str,
base_internal_url: &str,
w_id: &str,
) -> Result<(), String> {
let (s3_access_key, s3_secret_key) = token.rsplit_once('.').unwrap_or(("", token));
let (s3_endpoint_ssl, s3_endpoint) = base_internal_url
.split_once("://")
.unwrap_or(("http", &base_internal_url));
let s3_endpoint_ssl = s3_endpoint_ssl == "https";
conn.execute_batch(&format!(
"INSTALL httpfs; LOAD httpfs;
INSTALL azure; LOAD azure;
CREATE OR REPLACE SECRET s3_secret (
TYPE s3,
PROVIDER config,
KEY_ID '{s3_access_key}',
SECRET '{s3_secret_key}',
ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy',
URL_STYLE path,
USE_SSL {s3_endpoint_ssl}
);
CREATE OR REPLACE SECRET gcs_secret (
TYPE gcs,
KEY_ID '{s3_access_key}',
SECRET '{s3_secret_key}',
ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy',
USE_SSL {s3_endpoint_ssl}
);
",
))
.map_err(|e| format!("Error setting up S3 secret: {}", e.to_string()))
}
fn convert_prepare_args<'a>(
query_block_list: *const *const c_char,
query_block_list_count: usize,
token: *const c_char,
base_internal_url: *const c_char,
w_id: *const c_char,
) -> Result<(Vec<&'a str>, &'a str, &'a str, &'a str), String> {
let query_block_list = unsafe {
std::slice::from_raw_parts(query_block_list, query_block_list_count)
.iter()
.map(|q| {
CStr::from_ptr(*q).to_str().unwrap_or_else(|e| {
println!(
"Invalid query_block string pointer in duckdb ffi: {}",
e.to_string()
);
"Invalid query_block string pointer in duckdb ffi"
})
})
.collect::<Vec<_>>()
};
let token = unsafe { CStr::from_ptr(token) }
.to_str()
.map_err(|e| format!("Invalid token string: {}", e.to_string()))?;
let base_internal_url = unsafe { CStr::from_ptr(base_internal_url) }
.to_str()
.map_err(|e| format!("Invalid base_internal_url string: {}", e.to_string()))?;
let w_id = unsafe { CStr::from_ptr(w_id) }
.to_str()
.map_err(|e| format!("Invalid w_id string: {}", e.to_string()))?;
Ok((query_block_list, token, base_internal_url, w_id))
}
fn prepare_duckdb_internal(
query_block_list: Vec<&str>,
token: &str,
base_internal_url: &str,
w_id: &str,
) -> Result<String, String> {
let conn = duckdb::Connection::open_in_memory().map_err(|e| e.to_string())?;
setup_duckdb_connection(&conn, token, base_internal_url, w_id)?;
let mut results: Vec<PrepareQueryResult> = vec![];
// IMPORTANT: Setup statements (ATTACH, USE, INSTALL, etc.) are executed but intentionally
// do not produce a PrepareQueryResult entry. The frontend prepends these as connection setup
// before the actual user queries, and mapPrepareResults expects results.length to equal the
// number of user queries (not setup statements). If a new setup-like statement is added to
// the connection flow (e.g. in setup_duckdb_connection or transform_attach_ducklake) without
// also being caught by is_setup_statement, the result count will mismatch and the frontend
// will throw.
for query_block in &query_block_list {
if is_setup_statement(query_block) {
conn.execute_batch(query_block)
.map_err(|e| format!("Error executing setup statement: {}", e.to_string()))?;
continue;
}
let modified_query = replace_params_with_null(query_block);
// Validate the query parses correctly by preparing it
if let Err(e) = conn.prepare(&modified_query) {
results.push(PrepareQueryResult { columns: None, error: Some(e.to_string()) });
continue;
}
// DESCRIBE only works on queries that return result sets (SELECT, WITH, VALUES, TABLE,
// FROM). For non-returning statements (INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, etc.)
// we skip DESCRIBE and assume no columns.
if !is_describable_query(&modified_query) {
results.push(PrepareQueryResult { columns: Some(vec![]), error: None });
continue;
}
// Note: We have to use a DESCRIBE statement and cannot simply use the
// methods returned by .prepare() because they panic if the statement was
// not executed at least once (which we specifically do not want to do).
let describe_query = format!("DESCRIBE {}", modified_query);
match conn.prepare(&describe_query).and_then(|mut stmt| {
let rows = stmt.query_map([], |row| {
Ok(PrepareQueryColumnInfo {
name: row.get::<_, String>(0)?,
type_name: row.get::<_, String>(1)?,
})
})?;
rows.collect::<std::result::Result<Vec<_>, _>>()
}) {
Ok(columns) => {
results.push(PrepareQueryResult { columns: Some(columns), error: None });
}
Err(e) => {
results.push(PrepareQueryResult { columns: None, error: Some(e.to_string()) });
}
}
}
serde_json::to_string(&results).map_err(|e| e.to_string())
}
fn convert_args<'a>(
query_block_list: *const *const c_char,
query_block_list_count: usize,
@@ -170,38 +384,7 @@ fn run_duckdb_internal<'a>(
) -> Result<(String, Option<Vec<String>>), String> {
let conn = duckdb::Connection::open_in_memory().map_err(|e| e.to_string())?;
let (s3_access_key, s3_secret_key) = token.split_at(token.rfind('.').unwrap_or(0));
let s3_secret_key = &s3_secret_key[1..];
let (s3_endpoint_ssl, s3_endpoint) = base_internal_url
.split_once("://")
.unwrap_or(("http", &base_internal_url));
let s3_endpoint_ssl = match s3_endpoint_ssl {
"https" => true,
_ => false,
};
conn.execute_batch(&format!(
"INSTALL httpfs; LOAD httpfs;
INSTALL azure; LOAD azure;
CREATE OR REPLACE SECRET s3_secret (
TYPE s3,
PROVIDER config,
KEY_ID '{s3_access_key}',
SECRET '{s3_secret_key}',
ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy',
URL_STYLE path,
USE_SSL {s3_endpoint_ssl}
);
CREATE OR REPLACE SECRET gcs_secret (
TYPE gcs,
KEY_ID '{s3_access_key}',
SECRET '{s3_secret_key}',
ENDPOINT '{s3_endpoint}/api/w/{w_id}/s3_proxy',
USE_SSL {s3_endpoint_ssl}
);
",
))
.map_err(|e| format!("Error setting up S3 secret: {}", e.to_string()))?;
setup_duckdb_connection(&conn, token, base_internal_url, w_id)?;
let mut results: Vec<Vec<Box<RawValue>>> = vec![];
let mut column_order = None;
+12 -9
View File
@@ -45,7 +45,7 @@ use uuid::Uuid;
use windmill_common::error::Error;
use windmill_common::result_stream::append_result_stream_db;
use windmill_common::worker::{write_file, Connection, TMP_DIR};
use windmill_common::worker::{write_file, Connection, WINDMILL_DIR};
// ── Permission container ─────────────────────────────────────────────
@@ -151,7 +151,9 @@ static RUNTIME_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/FETCH
pub(crate) const WINDMILL_CLIENT: &str = include_str!("./windmill-client.js");
const ERROR_DIR: &str = const_format::concatcp!(TMP_DIR, "/native_errors");
lazy_static::lazy_static! {
static ref ERROR_DIR: String = format!("{}/native_errors", *WINDMILL_DIR);
}
lazy_static! {
static ref RE_PROXY: Regex =
@@ -263,14 +265,14 @@ fn capture_proxy(s: &str) -> Option<(String, Option<(String, String)>)> {
}
fn write_error_expr(expr: &str, uuid: &Uuid) {
if let Err(e) = std::fs::create_dir_all(ERROR_DIR) {
tracing::error!("failed to create error dir {ERROR_DIR}: {e}");
if let Err(e) = std::fs::create_dir_all(&*ERROR_DIR) {
tracing::error!("failed to create error dir {}: {e}", *ERROR_DIR);
return;
}
let dir_entries = match std::fs::read_dir(ERROR_DIR) {
let dir_entries = match std::fs::read_dir(&*ERROR_DIR) {
Ok(entries) => entries.count(),
Err(_) => {
tracing::error!("failed to read error dir {ERROR_DIR}");
tracing::error!("failed to read error dir {}", *ERROR_DIR);
return;
}
};
@@ -279,15 +281,16 @@ fn write_error_expr(expr: &str, uuid: &Uuid) {
tracing::info!("native error for job {uuid}: {expr}");
}
if dir_entries >= 100 {
tracing::info!("Too many error files in {ERROR_DIR}, skipping write");
tracing::info!("Too many error files in {}, skipping write", *ERROR_DIR);
return;
}
let path = format!("/{uuid}.js");
tracing::info!(
"nativets job {uuid} failed, writing error expr to {ERROR_DIR}/{path} for debugging: {path}"
"nativets job {uuid} failed, writing error expr to {}/{path} for debugging: {path}",
*ERROR_DIR
);
if let Err(e) = write_file(ERROR_DIR, &path, expr) {
if let Err(e) = write_file(&ERROR_DIR, &path, expr) {
tracing::error!("failed to write error expr to file {path}: {e}");
}
}
+2 -2
View File
@@ -47,7 +47,7 @@ use windmill_common::{
StripPath,
},
variables,
worker::{CLOUD_HOSTED, TMP_DIR},
worker::{CLOUD_HOSTED, WINDMILL_DIR},
PgDatabase,
};
@@ -1752,7 +1752,7 @@ async fn write_ssh_file(
var_path: &str,
) -> std::result::Result<std::path::PathBuf, (error::Error, std::path::PathBuf)> {
let id_file_name = format!(".ssh_id_priv_{}", Uuid::new_v4());
let loc = std::path::Path::new(TMP_DIR)
let loc = std::path::Path::new(&*WINDMILL_DIR)
.join("ssh_ids")
.join(id_file_name);
+1 -1
View File
@@ -380,7 +380,7 @@ pub fn spawn_test_worker(
std::fs::DirBuilder::new()
.recursive(true)
.create(windmill_worker::GO_BIN_CACHE_DIR)
.create(&*windmill_worker::GO_BIN_CACHE_DIR)
.expect("could not create initial worker dir");
let (tx, rx) = KillpillSender::new(1);
@@ -127,7 +127,7 @@ iface_no_lo: true
mount {
src: "{CACHE_DIR}"
dst: "/tmp/windmill/cache/powershell"
dst: "{CACHE_DIR}"
is_bind: true
rw: false
mandatory: false
@@ -33,9 +33,9 @@ use crate::{
read_and_check_result, start_child_process, transform_json, OccupancyMetrics,
},
handle_child::handle_child,
is_sandboxing_enabled,
python_executor::{create_dependencies_dir, handle_python_reqs, uv_pip_compile},
is_sandboxing_enabled, DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV, PROXY_ENVS,
PY_INSTALL_DIR, TZ_ENV,
DISABLE_NUSER, GIT_PATH, HOME_ENV, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, TZ_ENV,
};
use windmill_common::client::AuthedClient;
@@ -1184,7 +1184,7 @@ mount {{
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_ANSIBLE_CONTENT
.replace("{PY_INSTALL_DIR}", PY_INSTALL_DIR)
.replace("{PY_INSTALL_DIR}", &*PY_INSTALL_DIR)
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
+1 -1
View File
@@ -194,7 +194,7 @@ exit $exit_status
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut cmd_args = vec![
+3 -3
View File
@@ -737,7 +737,7 @@ async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<PulledCode
let bun_cache_path = format!(
"{}/{}.{}",
windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
*windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
path,
if is_tar { "tar" } else { "js" }
);
@@ -921,7 +921,7 @@ pub async fn compute_bundle_local_and_remote_path(
};
let hash = windmill_common::utils::calculate_hash(&input_src);
let local_path = format!("{BUN_BUNDLE_CACHE_DIR}/{hash}");
let local_path = format!("{}/{hash}", *BUN_BUNDLE_CACHE_DIR);
#[cfg(windows)]
let local_path = local_path.replace("/tmp", r"C:\tmp").replace("/", r"\");
@@ -1495,7 +1495,7 @@ try {{
},
),
)
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
+5 -6
View File
@@ -15,10 +15,6 @@ use tokio::process::Command;
use tokio::{fs::File, io::AsyncReadExt};
use windmill_common::flows::Step;
#[cfg(feature = "parquet")]
use windmill_types::s3::{LargeFileStorage, ObjectStoreResource, S3Object};
#[cfg(feature = "parquet")]
use windmill_object_store::get_etag_or_empty;
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt};
use windmill_common::worker::{
to_raw_value, update_ping_for_failed_init_script_query, write_file, Connection, Ping, PingType,
@@ -32,6 +28,10 @@ use windmill_common::{
utils::configure_client,
variables::ContextualVariable,
};
#[cfg(feature = "parquet")]
use windmill_object_store::get_etag_or_empty;
#[cfg(feature = "parquet")]
use windmill_types::s3::{LargeFileStorage, ObjectStoreResource, S3Object};
use anyhow::{anyhow, Result};
use windmill_parser_sql::{s3_mode_extension, S3ModeArgs, S3ModeFormat};
@@ -1090,7 +1090,7 @@ fn tentatively_improve_error(err: Error, executable: &str) -> Error {
pub async fn clean_cache() -> error::Result<()> {
tracing::info!("Started cleaning cache");
tokio::fs::remove_dir_all(ROOT_CACHE_DIR).await?;
tokio::fs::remove_dir_all(&*ROOT_CACHE_DIR).await?;
tracing::info!("Finished cleaning cache");
Ok(())
}
@@ -1557,4 +1557,3 @@ mod tests {
assert!(result.is_err());
}
}
+15 -19
View File
@@ -13,14 +13,11 @@ use itertools::Itertools;
#[cfg(feature = "csharp")]
use tokio::{fs::File, io::AsyncReadExt, process::Command};
#[cfg(feature = "csharp")]
use windmill_common::{
utils::calculate_hash,
worker::write_file,
};
use windmill_common::{utils::calculate_hash, worker::write_file};
use windmill_common::error::{self, Error};
#[cfg(feature = "csharp")]
use crate::global_cache::save_cache;
use windmill_common::error::{self, Error};
#[cfg(feature = "csharp")]
use windmill_queue::append_logs;
@@ -105,8 +102,8 @@ pub async fn generate_nuget_lockfile(
let mut gen_lockfile_cmd = Command::new(DOTNET_PATH.as_str());
gen_lockfile_cmd
.current_dir(job_dir)
.env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
.env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("MSBUILDDISABLENODEREUSE", "1")
@@ -367,8 +364,8 @@ async fn build_cs_proj(
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("HOME", HOME_ENV.as_str())
.env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
.env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("MSBUILDDISABLENODEREUSE", "1")
@@ -434,7 +431,7 @@ async fn build_cs_proj(
}
}
let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR);
let bin_path = format!("{}/{hash}", *CSHARP_CACHE_DIR);
#[cfg(unix)]
let target = format!("{job_dir}/Main");
#[cfg(windows)]
@@ -516,11 +513,10 @@ pub async fn handle_csharp_job(
inner_content,
requirements_o.unwrap_or(&String::new())
));
let bin_path = format!("{}/{hash}", CSHARP_CACHE_DIR);
let bin_path = format!("{}/{hash}", *CSHARP_CACHE_DIR);
let remote_path = format!("{CSHARP_OBJECT_STORE_PREFIX}{hash}");
let (cache, cache_logs) =
crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let (cache, cache_logs) = crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let cache_logs = if cache {
#[cfg(unix)]
@@ -591,11 +587,11 @@ pub async fn handle_csharp_job(
"run.config.proto",
&NSJAIL_CONFIG_RUN_CSHARP_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", CSHARP_CACHE_DIR)
.replace("{CACHE_DIR}", &*CSHARP_CACHE_DIR)
.replace("{CACHE_HASH}", &hash)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
@@ -608,8 +604,8 @@ pub async fn handle_csharp_job(
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
.env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("DOTNET_ROOT", DOTNET_ROOT.as_str())
@@ -640,8 +636,8 @@ pub async fn handle_csharp_job(
.envs(get_proxy_envs_for_lang(&ScriptLang::CSharp).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("DOTNET_CLI_HOME", CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{CSHARP_CACHE_DIR}/nuget"))
.env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
.env("NUGET_PACKAGES", format!("{}/nuget", *CSHARP_CACHE_DIR))
.env("DOTNET_CLI_TELEMETRY_OPTOUT", "true")
.env("DOTNET_NOLOGO", "true")
.env("DOTNET_ROOT", DOTNET_ROOT.as_str())
+4 -2
View File
@@ -443,7 +443,8 @@ try {{
}
let allow_read = format!(
"--allow-read=./,/tmp/windmill/cache/deno/,{}",
"--allow-read=./,{}/,{}",
*DENO_CACHE_DIR,
DENO_PATH.as_str()
);
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
@@ -504,7 +505,8 @@ try {{
*has_stream = handle_result.result_stream.is_some();
// logs.push_str(format!("execute: {:?}\n", start.elapsed().as_millis()).as_str());
if let Err(e) = tokio::fs::remove_dir_all(format!("{DENO_CACHE_DIR}/gen/file/{job_dir}")).await
if let Err(e) =
tokio::fs::remove_dir_all(format!("{}/gen/file/{job_dir}", *DENO_CACHE_DIR)).await
{
tracing::error!("failed to remove deno gen tmp cache dir: {}", e);
}
@@ -161,6 +161,22 @@ pub async fn do_duckdb(
let base_internal_url = client.base_internal_url.clone();
let w_id = job.workspace_id.clone();
if annotations.prepare {
let result = tokio::task::spawn_blocking(move || {
prepare_duckdb_ffi_safe(
query_block_list.iter().map(String::as_str),
&token,
&base_internal_url,
&w_id,
)
})
.await
.map_err(|e| Error::from(to_anyhow(e)))
.and_then(|r| r)?;
return Ok(result);
}
let result = tokio::task::spawn_blocking(move || {
run_duckdb_ffi_safe(
query_block_list.iter().map(String::as_str),
@@ -248,6 +264,18 @@ struct DuckDbFfiLib {
collect_first_row_only: bool,
) -> *mut c_char,
>,
prepare_duckdb_ffi: Option<
Symbol<
'static,
unsafe extern "C" fn(
query_block_list: *const *const c_char,
query_block_list_count: usize,
token: *const c_char,
base_internal_url: *const c_char,
w_id: *const c_char,
) -> *mut c_char,
>,
>,
free_cstr: Symbol<'static, unsafe extern "C" fn(string: *mut c_char) -> ()>,
}
@@ -307,8 +335,11 @@ impl DuckDbFfiLib {
}
}
let prepare_duckdb_ffi = unsafe { lib.get(b"prepare_duckdb_ffi").ok() };
Ok(DuckDbFfiLib {
run_duckdb_ffi: unsafe { lib.get(b"run_duckdb_ffi").map_err(to_anyhow)? },
prepare_duckdb_ffi,
free_cstr: unsafe { lib.get(b"free_cstr").map_err(to_anyhow)? },
})
}
@@ -388,6 +419,56 @@ fn run_duckdb_ffi_safe<'a>(
}
}
fn prepare_duckdb_ffi_safe<'a>(
query_block_list: impl Iterator<Item = &'a str>,
token: &str,
base_internal_url: &str,
w_id: &str,
) -> Result<Box<RawValue>> {
let query_block_list = query_block_list
.map(|s| {
CString::new(s).map_err(|e| {
Error::ExecutionErr(format!("Failed CString conversion: {}", e.to_string()))
})
})
.collect::<Result<Vec<_>>>()?;
let query_block_list = query_block_list
.iter()
.map(|s| s.as_ptr())
.collect::<Vec<_>>();
let token = CString::new(token).map_err(to_anyhow)?;
let base_internal_url = CString::new(base_internal_url).map_err(to_anyhow)?;
let w_id = CString::new(w_id).map_err(to_anyhow)?;
let lib = DuckDbFfiLib::get_singleton()?;
let prepare_fn = lib.prepare_duckdb_ffi.as_ref().ok_or_else(|| {
Error::InternalErr(
"prepare_duckdb_ffi not available in duckdb ffi library. Please update to the latest windmill_duckdb_ffi_lib.".to_string(),
)
})?;
let free_cstr = &lib.free_cstr;
let result_str = unsafe {
let ptr = prepare_fn(
query_block_list.as_ptr(),
query_block_list.len(),
token.as_ptr(),
base_internal_url.as_ptr(),
w_id.as_ptr(),
);
let str = CStr::from_ptr(ptr).to_string_lossy().to_string();
free_cstr(ptr);
str
};
if result_str.starts_with("ERROR") {
Err(Error::ExecutionErr(result_str[6..].to_string()))
} else {
Ok(serde_json::value::RawValue::from_string(result_str).map_err(to_anyhow)?)
}
}
struct ParsedAttachDbResource<'a> {
resource_path: &'a str,
name: &'a str,
+6 -4
View File
@@ -18,8 +18,8 @@ pub async fn build_tar_and_push(
custom_folder_name: Option<String>,
platform_agnostic: bool,
) -> error::Result<()> {
use windmill_object_store::object_store_reexports::Path;
use tokio::fs::create_dir_all;
use windmill_object_store::object_store_reexports::Path;
use crate::TAR_PYBASE_CACHE_DIR;
@@ -33,7 +33,7 @@ pub async fn build_tar_and_push(
folder.split("/").last().unwrap().to_owned()
};
let prefix = &format!("{TAR_PYBASE_CACHE_DIR}/{}", lang);
let prefix = &format!("{}/{}", *TAR_PYBASE_CACHE_DIR, lang);
let tar_path = format!("{prefix}/{folder_name}_tar.tar");
create_dir_all(prefix).await?;
@@ -197,7 +197,9 @@ pub async fn exists_in_cache(bin_path: &str, _remote_path: &str) -> bool {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = windmill_object_store::get_object_store().await {
return os
.get(&windmill_object_store::object_store_reexports::Path::from(_remote_path))
.get(&windmill_object_store::object_store_reexports::Path::from(
_remote_path,
))
.await
.is_ok();
}
@@ -221,7 +223,7 @@ pub async fn save_cache(
let file_to_cache = if is_dir {
let tar_path = format!(
"{}/tar/{}_tar.tar",
windmill_common::worker::ROOT_CACHE_DIR,
*windmill_common::worker::ROOT_CACHE_DIR,
local_cache_path
.split("/")
.last()
+12 -13
View File
@@ -2,6 +2,7 @@ use crate::{common::MaybeLock, get_proxy_envs_for_lang};
use std::{collections::HashMap, fs::DirBuilder, process::Stdio};
use windmill_common::scripts::ScriptLang;
use crate::global_cache::save_cache;
use itertools::Itertools;
use serde_json::value::RawValue;
use tokio::{
@@ -15,7 +16,6 @@ use windmill_common::{
utils::calculate_hash,
worker::{write_file, Connection, GoAnnotations},
};
use crate::global_cache::save_cache;
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
@@ -108,10 +108,9 @@ pub async fn handle_go_job(
.expect("could not create go job dir");
let hash = calculate_hash(&format!("{}{:?}v2", inner_content, &maybe_lock));
let bin_path = format!("{}/{hash}", GO_BIN_CACHE_DIR);
let bin_path = format!("{}/{hash}", *GO_BIN_CACHE_DIR);
let remote_path = format!("{GO_OBJECT_STORE_PREFIX}{hash}");
let (cache, cache_logs) =
crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let (cache, cache_logs) = crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let (skip_go_mod, skip_tidy) = if cache {
(true, true)
@@ -238,15 +237,15 @@ func Run(req Req) (interface{{}}, error){{
.env("GOPATH", {
#[cfg(unix)]
{
GO_CACHE_DIR
GO_CACHE_DIR.as_str()
}
#[cfg(windows)]
{
windows_gopath()
&windows_gopath()
}
})
.env("HOME", HOME_ENV.as_str())
.env("GOCACHE", GO_CACHE_DIR)
.env("GOCACHE", GO_CACHE_DIR.as_str())
.envs(PROXY_ENVS.clone())
.args(vec!["build", "main.go"])
.stdout(Stdio::piped())
@@ -347,7 +346,7 @@ func Run(req Req) (interface{{}}, error){{
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
@@ -384,11 +383,11 @@ func Run(req Req) (interface{{}}, error){{
.env("GOPATH", {
#[cfg(unix)]
{
GO_CACHE_DIR
GO_CACHE_DIR.as_str()
}
#[cfg(windows)]
{
windows_gopath()
&windows_gopath()
}
})
.env("HOME", HOME_ENV.as_str());
@@ -508,7 +507,7 @@ pub async fn install_go_dependencies(
#[cfg(windows)]
child_cmd.env("GOPATH", windows_gopath());
#[cfg(unix)]
child_cmd.env("GOPATH", GO_CACHE_DIR);
child_cmd.env("GOPATH", GO_CACHE_DIR.as_str());
#[cfg(windows)]
set_windows_env_vars(&mut child_cmd);
@@ -591,11 +590,11 @@ pub async fn install_go_dependencies(
.env("GOPATH", {
#[cfg(unix)]
{
GO_CACHE_DIR
GO_CACHE_DIR.as_str()
}
#[cfg(windows)]
{
windows_gopath()
&windows_gopath()
}
})
.args(vec!["mod", mod_command])
+17 -16
View File
@@ -1,5 +1,6 @@
use std::{collections::HashMap, path::PathBuf, process::Stdio};
use crate::global_cache::save_cache;
use anyhow::{anyhow, bail};
use async_recursion::async_recursion;
use itertools::Itertools;
@@ -15,7 +16,6 @@ use windmill_common::{
utils::calculate_hash,
worker::{copy_dir_recursively, write_file, Connection},
};
use crate::global_cache::save_cache;
use windmill_parser::Arg;
use windmill_parser_java::parse_java_sig_meta;
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
@@ -185,8 +185,8 @@ pub async fn resolve<'a>(
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
.env("HOME", JAVA_HOME_DIR)
.env("COURSIER_CACHE", COURSIER_CACHE_DIR)
.env("HOME", &*JAVA_HOME_DIR)
.env("COURSIER_CACHE", &*COURSIER_CACHE_DIR)
.envs(PROXY_ENVS.clone());
// Configure proxies
@@ -208,7 +208,7 @@ pub async fn resolve<'a>(
cmd.arg(&format!("-Dhttp.nonProxyHosts=\"{}\"", val));
}
}
cmd.arg(&format!("-Duser.home={}", JAVA_HOME_DIR));
cmd.arg(&format!("-Duser.home={}", *JAVA_HOME_DIR));
if metadata(TRUST_STORE_PATH.clone()).await.is_ok() {
cmd.args(&[
&format!("-Djavax.net.ssl.trustStore={}", *TRUST_STORE_PATH),
@@ -223,7 +223,7 @@ pub async fn resolve<'a>(
"--parallel",
&format!("{}", *JAVA_CONCURRENT_DOWNLOADS),
"--cache",
COURSIER_CACHE_DIR,
&*COURSIER_CACHE_DIR,
])
.args(&get_repos(job_id, w_id, conn).await)
.args(&deps.split("\n").collect_vec())
@@ -276,7 +276,8 @@ async fn install<'a>(
match (it.next(), it.next(), it.next()) {
(Some(group_id), Some(artifact_id), Some(version)) => {
let path = format!(
"{JAVA_REPOSITORY_DIR}/{}/{artifact_id}/{version}",
"{}/{}/{artifact_id}/{version}",
*JAVA_REPOSITORY_DIR,
group_id.replace(".", "/")
);
Ok(RequiredDependency {
@@ -312,7 +313,7 @@ async fn install<'a>(
metadata(TRUST_STORE_PATH.clone()).await,
);
let job_dir = job_dir.to_owned();
let fetch_dir = format!("{JAVA_CACHE_DIR}/tmp-fetch-{}", Uuid::new_v4());
let fetch_dir = format!("{}/tmp-fetch-{}", *JAVA_CACHE_DIR, Uuid::new_v4());
let fetch_dir2 = fetch_dir.clone();
par_install_language_dependencies_all_at_once(
deps,
@@ -334,8 +335,8 @@ async fn install<'a>(
cmd.env_clear()
.current_dir(&job_dir)
.env("PATH", PATH_ENV.as_str())
.env("HOME", JAVA_HOME_DIR)
.env("COURSIER_CACHE", COURSIER_CACHE_DIR)
.env("HOME", &*JAVA_HOME_DIR)
.env("COURSIER_CACHE", &*COURSIER_CACHE_DIR)
.envs(PROXY_ENVS.clone());
// Configure proxies
{
@@ -357,7 +358,7 @@ async fn install<'a>(
}
}
cmd.arg(&format!("-Duser.home={}", JAVA_HOME_DIR));
cmd.arg(&format!("-Duser.home={}", *JAVA_HOME_DIR));
if trust_store_metadata.is_ok() {
cmd.args(&[
&format!("-Djavax.net.ssl.trustStore={}", *TRUST_STORE_PATH),
@@ -400,7 +401,7 @@ async fn install<'a>(
if depth == 3 {
copy_dir_recursively(
&PathBuf::from(path),
&PathBuf::from(JAVA_REPOSITORY_DIR),
&PathBuf::from(&*JAVA_REPOSITORY_DIR),
)?;
return Ok(());
@@ -465,7 +466,7 @@ async fn compile<'a>(
let reserved_variables =
get_reserved_variables(job, &client.token, conn, parent_runnable_path.clone()).await?;
let hash = compute_hash(inner_content, *requirements_o);
let bin_path = format!("{}/{hash}", JAVA_CACHE_DIR);
let bin_path = format!("{}/{hash}", *JAVA_CACHE_DIR);
let remote_path = format!("java_jar/{hash}");
let (cache, ..) = crate::global_cache::load_cache(&bin_path, &remote_path, true).await;
@@ -501,7 +502,7 @@ async fn compile<'a>(
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
.env("HOME", JAVA_HOME_DIR)
.env("HOME", &*JAVA_HOME_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
@@ -604,7 +605,7 @@ async fn run<'a>(
"run.config.proto",
&NSJAIL_CONFIG_RUN_JAVA_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", JAVA_CACHE_DIR)
.replace("{CACHE_DIR}", &*JAVA_CACHE_DIR)
.replace("{SHARED_MOUNT}", &shared_mount)
// .replace("{CACHED_TARGET}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
@@ -613,7 +614,7 @@ async fn run<'a>(
cmd.env_clear()
.current_dir(job_dir)
.env("PATH", PATH_ENV.as_str())
.env("HOME", JAVA_HOME_DIR)
.env("HOME", &*JAVA_HOME_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
@@ -671,7 +672,7 @@ async fn run<'a>(
cmd.env_clear()
.current_dir(job_dir.to_owned())
.env("PATH", PATH_ENV.as_str())
.env("HOME", JAVA_HOME_DIR)
.env("HOME", &*JAVA_HOME_DIR)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables);
+3 -3
View File
@@ -16,8 +16,8 @@ use crate::{
build_command_with_isolation, create_args_and_out_file, get_reserved_variables,
read_result, start_child_process, OccupancyMetrics, DEV_CONF_NSJAIL,
},
get_proxy_envs_for_lang, handle_child, is_sandboxing_enabled, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV,
TRACING_PROXY_CA_CERT_PATH,
get_proxy_envs_for_lang, handle_child, is_sandboxing_enabled, DISABLE_NUSER, NSJAIL_PATH,
PATH_ENV, TRACING_PROXY_CA_CERT_PATH,
};
use windmill_common::client::AuthedClient;
use windmill_common::scripts::ScriptLang;
@@ -253,7 +253,7 @@ async fn run<'a>(
.replace("{NU_PATH}", &NU_PATH)
.replace("{SHARED_MOUNT}", &shared_mount)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
+5 -5
View File
@@ -159,7 +159,7 @@ try {
async fn scan_module_directories() -> Result<HashMap<String, String>, Error> {
let mut module_dirs = HashMap::new();
let cache_dir = std::path::Path::new(POWERSHELL_CACHE_DIR);
let cache_dir = std::path::Path::new(&*POWERSHELL_CACHE_DIR);
if let Ok(entries) = fs::read_dir(cache_dir) {
for entry in entries {
@@ -391,7 +391,7 @@ pub async fn handle_powershell_job(
.join(", ");
let install_string = generate_powershell_install_code()
.replace("{path}", POWERSHELL_CACHE_DIR)
.replace("{path}", &*POWERSHELL_CACHE_DIR)
.replace("{job_id}", &job.id.to_string())
.replace("{has_private_repo}", &format!("${has_private_repo}"))
.replace("{has_credentials}", &format!("${has_credentials}"))
@@ -442,7 +442,7 @@ $PSModulePathBackup = $env:PSModulePath
$env:PSModulePath = \"$PSHome/Modules\"
Get-Module -ListAvailable | Import-Module
$env:PSModulePath = \"{}:$PSModulePathBackup\"",
POWERSHELL_CACHE_DIR
*POWERSHELL_CACHE_DIR
);
#[cfg(windows)]
@@ -452,7 +452,7 @@ $PSModulePathBackup = $env:PSModulePath
$env:PSModulePath = \"C:\\Program Files\\PowerShell\\7\\Modules\"
Get-Module -ListAvailable | Import-Module
$env:PSModulePath = \"{};$PSModulePathBackup\"",
POWERSHELL_CACHE_DIR
*POWERSHELL_CACHE_DIR
);
// NOTE: powershell error handling / termination is quite tricky compared to bash
@@ -525,7 +525,7 @@ $env:PSModulePath = \"{};$PSModulePathBackup\"",
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{CACHE_DIR}", POWERSHELL_CACHE_DIR),
.replace("{CACHE_DIR}", &*POWERSHELL_CACHE_DIR),
)?;
let cmd_args = vec![
"--config",
@@ -278,7 +278,7 @@ pub async fn uv_pip_compile(
"requirements.txt",
// Target to /tmp/windmill/cache/uv
"--cache-dir",
UV_CACHE_DIR,
&*UV_CACHE_DIR,
];
args.extend(["-p", &py_version_str, "--python-preference", "only-managed"]);
@@ -805,7 +805,7 @@ mount {{
"run.config.proto",
&NSJAIL_CONFIG_RUN_PYTHON3_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{PY_INSTALL_DIR}", PY_INSTALL_DIR)
.replace("{PY_INSTALL_DIR}", &*PY_INSTALL_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
@@ -815,7 +815,7 @@ mount {{
"{ADDITIONAL_PYTHON_PATHS}",
additional_python_paths_folders.as_str(),
)
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL),
)?;
} else {
@@ -1410,10 +1410,10 @@ async fn spawn_uv_install(
&nsjail_proto,
NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
.replace("{WORKER_DIR}", worker_dir)
.replace("{PY_INSTALL_DIR}", &PY_INSTALL_DIR)
.replace("{PY_INSTALL_DIR}", &*PY_INSTALL_DIR)
.replace("{TARGET_DIR}", &venv_p)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.as_str(),
)?;
+10 -11
View File
@@ -18,6 +18,8 @@ use windmill_common::{
use anyhow::{anyhow, bail};
use windmill_queue::append_logs;
#[cfg(unix)]
use crate::python_executor::UV_PATH;
use crate::{
common::{start_child_process, OccupancyMetrics},
handle_child::handle_child,
@@ -25,8 +27,6 @@ use crate::{
HOME_ENV, INSTANCE_PYTHON_VERSION, PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, UV_CACHE_DIR,
WIN_ENVS,
};
#[cfg(unix)]
use crate::python_executor::UV_PATH;
impl From<PyV> for PyVAlias {
fn from(value: PyV) -> Self {
@@ -234,7 +234,8 @@ impl PyV {
pub(crate) fn to_cache_dir(&self, ignore_patch: bool) -> String {
use windmill_common::worker::ROOT_CACHE_DIR;
format!(
"{ROOT_CACHE_DIR}{}",
"{}{}",
*ROOT_CACHE_DIR,
self.to_cache_dir_top_level(ignore_patch)
)
}
@@ -311,7 +312,7 @@ impl PyV {
Command::new(uv_cmd)
.env_clear()
.envs(WIN_ENVS.to_vec())
.env("UV_CACHE_DIR", UV_CACHE_DIR)
.env("UV_CACHE_DIR", &*UV_CACHE_DIR)
.args([
"python",
"list",
@@ -539,8 +540,8 @@ impl PyV {
])
// TODO: Do we need these?
.envs([
("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR),
("UV_CACHE_DIR", UV_CACHE_DIR),
("UV_PYTHON_INSTALL_DIR", &*PY_INSTALL_DIR),
("UV_CACHE_DIR", &*UV_CACHE_DIR),
])
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@@ -630,11 +631,9 @@ impl PyV {
"--system",
"--python-preference=only-managed",
])
.envs([
("UV_PYTHON_INSTALL_DIR", PY_INSTALL_DIR),
("UV_PYTHON_PREFERENCE", "only-managed"),
("UV_CACHE_DIR", UV_CACHE_DIR),
])
.env("UV_PYTHON_INSTALL_DIR", &*PY_INSTALL_DIR)
.env("UV_PYTHON_PREFERENCE", "only-managed")
.env("UV_CACHE_DIR", &*UV_CACHE_DIR)
// .stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
+8 -9
View File
@@ -1,7 +1,6 @@
use std::{collections::HashMap, process::Stdio};
use anyhow::anyhow;
use const_format::concatcp;
use itertools::Itertools;
use regex::Regex;
use tokio::{
@@ -122,7 +121,7 @@ pub async fn prepare<'a>(
.write_all(&wrap(inner_content)?.into_bytes())
.await?;
let mini_wm_path = format!("{RUBY_CACHE_DIR}/gems/windmill-internal/windmill");
let mini_wm_path = format!("{}/gems/windmill-internal/windmill", *RUBY_CACHE_DIR);
if !std::fs::metadata(&mini_wm_path).is_ok() {
fs::create_dir_all(&mini_wm_path).await?;
@@ -339,7 +338,7 @@ Your Gemfile syntax will continue to work as-is."
&NSJAIL_CONFIG_LOCK_RUBY_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL), // .replace("{BUILD}", &build_dir),
)?;
let mut cmd = Command::new(NSJAIL_PATH.as_str());
@@ -588,7 +587,7 @@ async fn install<'a>(
// 123...zx-activesupport-8.0.2
// ^^^^^^^^ hash based on source and type (GEM or GIT)
let handle = format!("{}-{}-{}", hash, pkg, version);
let path = format!("{RUBY_CACHE_DIR}/gems/{}", &handle);
let path = format!("{}/gems/{}", *RUBY_CACHE_DIR, &handle);
deps.push(RequiredDependency {
path,
@@ -632,7 +631,7 @@ async fn install<'a>(
&NSJAIL_CONFIG_DOWNLOAD_RUBY_CONTENT
.replace("{TARGET}", &dependency.path)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL), // .replace("{BUILD}", &build_dir),
)?;
let mut cmd = Command::new(NSJAIL_PATH.as_str());
@@ -741,9 +740,9 @@ async fn install<'a>(
};
// Include builtin windmill client
{
const WM_INTERNAL: &str = concatcp!(RUBY_CACHE_DIR, "/gems/windmill-internal");
res.top_level_paths.push(WM_INTERNAL.to_owned());
res.rubylib += format!(":{WM_INTERNAL}").as_str();
let wm_internal = format!("{}/gems/windmill-internal", *RUBY_CACHE_DIR);
res.top_level_paths.push(wm_internal.clone());
res.rubylib += format!(":{wm_internal}").as_str();
}
Ok(res)
}
@@ -800,7 +799,7 @@ mount {{
.replace("{JOB_DIR}", job_dir)
.replace("{SHARED_MOUNT}", &shared_mount)
.replace("{SHARED_DEPENDENCIES}", &shared_deps)
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)?;
+15 -12
View File
@@ -5,6 +5,7 @@ use std::{collections::HashMap, process::Stdio};
use uuid::Uuid;
use windmill_parser_rust::parse_rust_deps_into_manifest;
use crate::global_cache::save_cache;
use itertools::Itertools;
use tokio::{
fs::{create_dir_all, File},
@@ -16,7 +17,6 @@ use windmill_common::{
utils::calculate_hash,
worker::{write_file, Connection},
};
use crate::global_cache::save_cache;
use windmill_queue::MiniPulledJob;
use windmill_queue::{append_logs, CanceledBy};
@@ -337,7 +337,7 @@ async fn get_build_dir(
if !is_sandboxing_enabled() {
// If nsjail is disabled then entire worker has shared build directory
// It drastically improves cache hit-rate.
Some((format!("{RUST_CACHE_DIR}/build/{worker_name}"), true))
Some((format!("{}/build/{worker_name}", *RUST_CACHE_DIR), true))
} else {
// If nsjail is enabled, having global shared directory is vulnerability and target for an attack
// Instead we either:
@@ -345,7 +345,8 @@ async fn get_build_dir(
// 2. If user is not known or something else goes wrong - use random build dir. This is equivalent to no cache at all.
Some((
format!(
"{RUST_CACHE_DIR}/build/{}@{}@{}",
"{}/build/{}@{}@{}",
*RUST_CACHE_DIR,
&job.workspace_id,
p.replace('/', "."),
&job.created_by
@@ -355,7 +356,10 @@ async fn get_build_dir(
}
}
})
.unwrap_or((format!("{RUST_CACHE_DIR}/build/{}", Uuid::new_v4()), false));
.unwrap_or((
format!("{}/build/{}", *RUST_CACHE_DIR, Uuid::new_v4()),
false,
));
{
let (t, r, g) = (
@@ -449,7 +453,7 @@ pub async fn build_rust_crate(
is_preview: bool,
) -> error::Result<String> {
ensure_rust_runtime_dirs();
let bin_path = format!("{}/{hash}", RUST_CACHE_DIR);
let bin_path = format!("{}/{hash}", *RUST_CACHE_DIR);
let build_dir = get_build_dir(job, job_dir, conn, worker_name, is_preview).await?;
@@ -459,9 +463,9 @@ pub async fn build_rust_crate(
"download.config.proto",
&NSJAIL_CONFIG_COMPILE_RUST_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", RUST_CACHE_DIR)
.replace("{CACHE_DIR}", &*RUST_CACHE_DIR)
.replace("{CARGO_HOME}", CARGO_HOME.as_str())
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{BUILD}", &build_dir),
)?;
@@ -605,14 +609,13 @@ pub async fn handle_rust_job(
check_executor_binary_exists("cargo", CARGO_PATH.as_str(), "rust")?;
let hash = compute_rust_hash(inner_content, requirements_o);
let bin_path = format!("{}/{hash}", RUST_CACHE_DIR);
let bin_path = format!("{}/{hash}", *RUST_CACHE_DIR);
let remote_path = format!("{RUST_OBJECT_STORE_PREFIX}{hash}");
let reserved_variables =
get_reserved_variables(job, &client.token, conn, parent_runnable_path).await?;
let (cache, cache_logs) =
crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let (cache, cache_logs) = crate::global_cache::load_cache(&bin_path, &remote_path, false).await;
let cache_logs = if cache {
let target = format!("{job_dir}/main");
@@ -669,10 +672,10 @@ pub async fn handle_rust_job(
"run.config.proto",
&NSJAIL_CONFIG_RUN_RUST_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", RUST_CACHE_DIR)
.replace("{CACHE_DIR}", &*RUST_CACHE_DIR)
.replace("{CACHE_HASH}", &hash)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{TRACING_PROXY_CA_CERT_PATH}", TRACING_PROXY_CA_CERT_PATH)
.replace("{TRACING_PROXY_CA_CERT_PATH}", &*TRACING_PROXY_CA_CERT_PATH)
.replace("#{DEV}", DEV_CONF_NSJAIL)
.replace("{SHARED_MOUNT}", shared_mount),
)?;
+35 -34
View File
@@ -37,7 +37,7 @@ use windmill_common::{
utils::{create_directory_async, WarnAfterExt},
worker::{
make_pull_query, write_file, Connection, HttpClient, MAX_TIMEOUT,
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
MIN_PERIODIC_SCRIPT_INTERVAL_SECONDS, ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, WINDMILL_DIR,
},
worker_group_job_stats::JobStatsMap,
KillpillSender,
@@ -47,7 +47,6 @@ use windmill_common::{
use windmill_common::ee_oss::LICENSE_KEY_VALID;
use anyhow::Result;
use const_format::concatcp;
#[cfg(feature = "prometheus")]
use prometheus::IntCounter;
@@ -196,45 +195,47 @@ use windmill_common::bench::{benchmark_init, benchmark_verify, BenchmarkInfo, Be
use windmill_common::add_time;
pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_10");
pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_11");
pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_12");
pub const PY313_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_3_13");
lazy_static::lazy_static! {
pub static ref PY310_CACHE_DIR: String = format!("{}python_3_10", *ROOT_CACHE_DIR);
pub static ref PY311_CACHE_DIR: String = format!("{}python_3_11", *ROOT_CACHE_DIR);
pub static ref PY312_CACHE_DIR: String = format!("{}python_3_12", *ROOT_CACHE_DIR);
pub static ref PY313_CACHE_DIR: String = format!("{}python_3_13", *ROOT_CACHE_DIR);
pub const TAR_JAVA_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar/java");
pub static ref TAR_JAVA_CACHE_DIR: String = format!("{}tar/java", *ROOT_CACHE_DIR);
pub const UV_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "uv");
pub const PY_INSTALL_DIR: &str = concatcp!(ROOT_CACHE_DIR, "py_runtime");
pub const TAR_PYBASE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "tar");
pub const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
pub const DENO_CACHE_DIR_DEPS: &str = concatcp!(ROOT_CACHE_DIR, "deno/deps");
pub const DENO_CACHE_DIR_NPM: &str = concatcp!(ROOT_CACHE_DIR, "deno/npm");
pub static ref UV_CACHE_DIR: String = format!("{}uv", *ROOT_CACHE_DIR);
pub static ref PY_INSTALL_DIR: String = format!("{}py_runtime", *ROOT_CACHE_DIR);
pub static ref TAR_PYBASE_CACHE_DIR: String = format!("{}tar", *ROOT_CACHE_DIR);
pub static ref DENO_CACHE_DIR: String = format!("{}deno", *ROOT_CACHE_DIR);
pub static ref DENO_CACHE_DIR_DEPS: String = format!("{}deno/deps", *ROOT_CACHE_DIR);
pub static ref DENO_CACHE_DIR_NPM: String = format!("{}deno/npm", *ROOT_CACHE_DIR);
pub const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
pub const RUST_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "rust");
pub const NU_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "nu");
pub const CSHARP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "csharp");
pub static ref GO_CACHE_DIR: String = format!("{}go", *ROOT_CACHE_DIR);
pub static ref RUST_CACHE_DIR: String = format!("{}rust", *ROOT_CACHE_DIR);
pub static ref NU_CACHE_DIR: String = format!("{}nu", *ROOT_CACHE_DIR);
pub static ref CSHARP_CACHE_DIR: String = format!("{}csharp", *ROOT_CACHE_DIR);
// Java
pub const JAVA_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "java");
pub const COURSIER_CACHE_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/coursier-cache");
pub const JAVA_REPOSITORY_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/repository");
pub const JAVA_HOME_DIR: &str = concatcp!(JAVA_CACHE_DIR, "/home");
// Java
pub static ref JAVA_CACHE_DIR: String = format!("{}java", *ROOT_CACHE_DIR);
pub static ref COURSIER_CACHE_DIR: String = format!("{}/coursier-cache", *JAVA_CACHE_DIR);
pub static ref JAVA_REPOSITORY_DIR: String = format!("{}/repository", *JAVA_CACHE_DIR);
pub static ref JAVA_HOME_DIR: String = format!("{}/home", *JAVA_CACHE_DIR);
// Ruby
pub const RUBY_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "ruby");
// Ruby
pub static ref RUBY_CACHE_DIR: String = format!("{}ruby", *ROOT_CACHE_DIR);
// for related places search: ADD_NEW_LANG
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "bun");
pub const BUN_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
pub const BUN_CODEBASE_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "script_bundle");
// for related places search: ADD_NEW_LANG
pub static ref BUN_CACHE_DIR: String = format!("{}bun", *ROOT_CACHE_NOMOUNT_DIR);
pub static ref BUN_BUNDLE_CACHE_DIR: String = format!("{}bun", *ROOT_CACHE_DIR);
pub static ref BUN_CODEBASE_BUNDLE_CACHE_DIR: String = format!("{}script_bundle", *ROOT_CACHE_NOMOUNT_DIR);
pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin");
pub const POWERSHELL_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "powershell");
pub const COMPOSER_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "composer");
pub static ref GO_BIN_CACHE_DIR: String = format!("{}gobin", *ROOT_CACHE_DIR);
pub static ref POWERSHELL_CACHE_DIR: String = format!("{}powershell", *ROOT_CACHE_DIR);
pub static ref COMPOSER_CACHE_DIR: String = format!("{}composer", *ROOT_CACHE_DIR);
pub const TRACING_PROXY_CA_CERT_PATH: &str =
concatcp!(ROOT_CACHE_NOMOUNT_DIR, "tracing_proxy_ca.pem");
pub static ref TRACING_PROXY_CA_CERT_PATH: String =
format!("{}tracing_proxy_ca.pem", *ROOT_CACHE_NOMOUNT_DIR);
}
const NUM_SECS_PING: u64 = 5;
const NUM_SECS_READINGS: u64 = 60;
@@ -1375,7 +1376,7 @@ pub async fn run_worker(
let start_time = Instant::now();
let worker_dir = format!("{TMP_DIR}/{worker_name}");
let worker_dir = format!("{}/{worker_name}", *WINDMILL_DIR);
tracing::debug!(worker = %worker_name, hostname = %hostname, worker_dir = %worker_dir, "Creating worker dir");
#[cfg(feature = "python")]
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.648.0";
export const VERSION = "v1.649.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -67,7 +67,7 @@ export {
workspaceAdd,
};
export const VERSION = "1.648.0";
export const VERSION = "1.649.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";
+694
View File
@@ -0,0 +1,694 @@
Total messages: 23 (Errors: 0, Warnings: 1)
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] layoutNodes same nodes @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:516
[DEBUG] [vite] connecting... @ http://localhost:5183/@vite/client:730
[DEBUG] [vite] connected. @ http://localhost:5183/@vite/client:832
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] layoutNodes 3 @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:521
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [Input]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:535
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-Input-Result",
"sources": [
"Input"
],
"targets": [
"Result"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:536
[LOG] cancelling request: {reason: saveAndClear, abortController: undefined} @ http://localhost:5183/src/lib/components/copilot/chat/AIChatManager.svelte.ts?t=1772535251800:1024
[LOG] loadFlow @ http://localhost:5183/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte?t=1772544066222:100
[LOG] ELK result: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34,
"$H": 277,
"x": 12,
"y": 12
},
{
"id": "Input",
"width": 275,
"height": 34,
"$H": 279,
"x": 12,
"y": 108
},
{
"id": "Result",
"width": 275,
"height": 34,
"$H": 281,
"x": 12,
"y": 204
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
],
"sections": [
{
"id": "e-Trigger-Input_s0",
"startPoint": {
"x": 150,
"y": 46
},
"endPoint": {
"x": 150,
"y": 108
},
"incomingShape": "Trigger",
"outgoingShape": "Input"
}
],
"container": "root"
},
{
"id": "e-Input-Result",
"sources": [
"Input"
],
"targets": [
"Result"
],
"sections": [
{
"id": "e-Input-Result_s0",
"startPoint": {
"x": 150,
"y": 142
},
"endPoint": {
"x": 150,
"y": 204
},
"incomingShape": "Input",
"outgoingShape": "Result"
}
],
"container": "root"
}
],
"$H": 13,
"x": 0,
"y": 0,
"width": 299,
"height": 250
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:540
[LOG] Welcome back admin to testing @ http://localhost:5183/src/routes/(root)/+layout.svelte?t=1772535251800:68
[WARNING] using deprecated parameters for the initialization function; pass a single object instead @ http://localhost:5183/node_modules/@sveltejs/kit/src/runtime/client/client.js?v=5452fe86:3211
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] layoutNodes 14 @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:521
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [a-end], a(module) ← [Input], a-end(branchAllEnd) ← [c,d,g], a-branch-0(branchAllStart) ← [a], b(module) ← [a-branch-0], c(module) ← [b], a-branch-1(branchAllStart) ← [a], d(module) ← [a-branch-1], a-branch-2(branchAllStart) ← [a], e(module) ← [a-branch-2], f(module) ← [e], g(module) ← [f]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:535
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
},
{
"id": "a-end",
"width": 275,
"height": 34
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.spacing.nodeNode": "50",
"elk.separateConnectedComponents": "false",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
"elk.layered.nodePlacement.bk.fixedAlignment": "LEFTUP"
},
"children": [
{
"id": "a-branch-0",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "b",
"width": 275,
"height": 34
},
{
"id": "c",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-b-c",
"sources": [
"b"
],
"targets": [
"c"
]
}
]
},
{
"id": "a-branch-1",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "d",
"width": 275,
"height": 34
}
],
"edges": []
},
{
"id": "a-branch-2",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "e",
"width": 275,
"height": 34
},
{
"id": "f",
"width": 275,
"height": 34
},
{
"id": "g",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-e-f",
"sources": [
"e"
],
"targets": [
"f"
]
},
{
"id": "e-f-g",
"sources": [
"f"
],
"targets": [
"g"
]
}
]
}
]
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
]
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
]
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:536
[LOG] ELK result: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34,
"$H": 352,
"x": 385,
"y": 12
},
{
"id": "Input",
"width": 275,
"height": 34,
"$H": 354,
"x": 385,
"y": 108
},
{
"id": "Result",
"width": 275,
"height": 34,
"$H": 356,
"x": 385,
"y": 636
},
{
"id": "a-end",
"width": 275,
"height": 34,
"$H": 358,
"x": 385,
"y": 540
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.spacing.nodeNode": "50",
"elk.separateConnectedComponents": "false",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
"elk.layered.nodePlacement.bk.fixedAlignment": "LEFTUP"
},
"children": [
{
"id": "a-branch-0",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "b",
"width": 275,
"height": 34,
"$H": 364,
"x": 12,
"y": 12
},
{
"id": "c",
"width": 275,
"height": 34,
"$H": 366,
"x": 12,
"y": 108
}
],
"edges": [
{
"id": "e-b-c",
"sources": [
"b"
],
"targets": [
"c"
],
"sections": [
{
"id": "e-b-c_s0",
"startPoint": {
"x": 149.5,
"y": 46
},
"endPoint": {
"x": 149.5,
"y": 108
},
"incomingShape": "b",
"outgoingShape": "c"
}
],
"container": "a-branch-0"
}
],
"$H": 362,
"x": 12,
"y": 60,
"width": 299,
"height": 154
},
{
"id": "a-branch-1",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "d",
"width": 275,
"height": 34,
"$H": 370,
"x": 12,
"y": 12
}
],
"edges": [],
"$H": 368,
"x": 361,
"y": 108,
"width": 299,
"height": 58
},
{
"id": "a-branch-2",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "e",
"width": 275,
"height": 34,
"$H": 374,
"x": 12,
"y": 12
},
{
"id": "f",
"width": 275,
"height": 34,
"$H": 376,
"x": 12,
"y": 108
},
{
"id": "g",
"width": 275,
"height": 34,
"$H": 378,
"x": 12,
"y": 204
}
],
"edges": [
{
"id": "e-e-f",
"sources": [
"e"
],
"targets": [
"f"
],
"sections": [
{
"id": "e-e-f_s0",
"startPoint": {
"x": 149.5,
"y": 46
},
"endPoint": {
"x": 149.5,
"y": 108
},
"incomingShape": "e",
"outgoingShape": "f"
}
],
"container": "a-branch-2"
},
{
"id": "e-f-g",
"sources": [
"f"
],
"targets": [
"g"
],
"sections": [
{
"id": "e-f-g_s0",
"startPoint": {
"x": 149.5,
"y": 142
},
"endPoint": {
"x": 149.5,
"y": 204
},
"incomingShape": "f",
"outgoingShape": "g"
}
],
"container": "a-branch-2"
}
],
"$H": 372,
"x": 710,
"y": 12,
"width": 299,
"height": 250
}
],
"$H": 360,
"x": 12,
"y": 204,
"width": 1021,
"height": 274
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
],
"sections": [
{
"id": "e-Trigger-Input_s0",
"startPoint": {
"x": 523,
"y": 46
},
"endPoint": {
"x": 523,
"y": 108
},
"incomingShape": "Trigger",
"outgoingShape": "Input"
}
],
"container": "root"
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
],
"sections": [
{
"id": "e-a-end-Result_s0",
"startPoint": {
"x": 523,
"y": 574
},
"endPoint": {
"x": 523,
"y": 636
},
"incomingShape": "a-end",
"outgoingShape": "Result"
}
],
"container": "root"
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
],
"sections": [
{
"id": "e-Input-a_s0",
"startPoint": {
"x": 523,
"y": 142
},
"endPoint": {
"x": 523,
"y": 204
},
"incomingShape": "Input",
"outgoingShape": "a"
}
],
"container": "root"
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
],
"sections": [
{
"id": "e-a-a-end_s0",
"startPoint": {
"x": 523,
"y": 478
},
"endPoint": {
"x": 523,
"y": 540
},
"incomingShape": "a",
"outgoingShape": "a-end"
}
],
"container": "root"
}
],
"$H": 350,
"x": 0,
"y": 0,
"width": 1045,
"height": 682
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:540
[DEBUG] Building graph @ http://localhost:5183/src/lib/components/graph/graphBuilder.svelte.ts?t=1772535251657:65
[DEBUG] layoutNodes same nodes @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772544066222:516
+425
View File
@@ -0,0 +1,425 @@
Total messages: 36 (Errors: 0, Warnings: 1)
Returning 22 messages for level "info"
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [Input]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-Input-Result",
"sources": [
"Input"
],
"targets": [
"Result"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:532
[LOG] cancelling request: {reason: saveAndClear, abortController: undefined} @ http://localhost:5183/src/lib/components/copilot/chat/AIChatManager.svelte.ts?t=1772535251800:1024
[LOG] loadFlow @ http://localhost:5183/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte?t=1772543608757:100
[LOG] Welcome back admin to testing @ http://localhost:5183/src/routes/(root)/+layout.svelte?t=1772535251800:68
[WARNING] using deprecated parameters for the initialization function; pass a single object instead @ http://localhost:5183/node_modules/@sveltejs/kit/src/runtime/client/client.js?v=5452fe86:3211
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [a-end], a(module) ← [Input], a-end(branchAllEnd) ← [c,d,g], a-branch-0(branchAllStart) ← [a], b(module) ← [a-branch-0], c(module) ← [b], a-branch-1(branchAllStart) ← [a], d(module) ← [a-branch-1], a-branch-2(branchAllStart) ← [a], e(module) ← [a-branch-2], f(module) ← [e], g(module) ← [f]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
},
{
"id": "a-end",
"width": 275,
"height": 34
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "a-branch-0",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "b",
"width": 275,
"height": 34
},
{
"id": "c",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-b-c",
"sources": [
"b"
],
"targets": [
"c"
]
}
]
},
{
"id": "a-branch-1",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "d",
"width": 275,
"height": 34
}
],
"edges": []
},
{
"id": "a-branch-2",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "e",
"width": 275,
"height": 34
},
{
"id": "f",
"width": 275,
"height": 34
},
{
"id": "g",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-e-f",
"sources": [
"e"
],
"targets": [
"f"
]
},
{
"id": "e-f-g",
"sources": [
"f"
],
"targets": [
"g"
]
}
]
}
]
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
]
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
]
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:532
[LOG] RawScript module b without assets field, parsing @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:149
[LOG] Parsing assets for RawScript module b @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:121
[LOG] RawScript module c without assets field, parsing @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:149
[LOG] Parsing assets for RawScript module c @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:121
[LOG] RawScript module d without assets field, parsing @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:149
[LOG] Parsing assets for RawScript module d @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:121
[LOG] RawScript module e without assets field, parsing @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:149
[LOG] Parsing assets for RawScript module e @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:121
[LOG] RawScript module f without assets field, parsing @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:149
[LOG] Parsing assets for RawScript module f @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:121
[LOG] RawScript module g without assets field, parsing @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:149
[LOG] Parsing assets for RawScript module g @ http://localhost:5183/src/lib/components/flows/FlowAssetsHandler.svelte?t=1772535251800:121
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [a-end], a(module) ← [Input], a-end(branchAllEnd) ← [c,d,g], a-branch-0(branchAllStart) ← [a], b(module) ← [a-branch-0], c(module) ← [b], a-branch-1(branchAllStart) ← [a], d(module) ← [a-branch-1], a-branch-2(branchAllStart) ← [a], e(module) ← [a-branch-2], f(module) ← [e], g(module) ← [f]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
},
{
"id": "a-end",
"width": 275,
"height": 34
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "a-branch-0",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "b",
"width": 275,
"height": 34
},
{
"id": "c",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-b-c",
"sources": [
"b"
],
"targets": [
"c"
]
}
]
},
{
"id": "a-branch-1",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "d",
"width": 275,
"height": 34
}
],
"edges": []
},
{
"id": "a-branch-2",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "e",
"width": 275,
"height": 34
},
{
"id": "f",
"width": 275,
"height": 34
},
{
"id": "g",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-e-f",
"sources": [
"e"
],
"targets": [
"f"
]
},
{
"id": "e-f-g",
"sources": [
"f"
],
"targets": [
"g"
]
}
]
}
]
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
]
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
]
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:532
+149
View File
@@ -0,0 +1,149 @@
Total messages: 15 (Errors: 0, Warnings: 0)
Returning 6 messages for level "info"
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [Input]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-Input-Result",
"sources": [
"Input"
],
"targets": [
"Result"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:532
[LOG] cancelling request: {reason: saveAndClear, abortController: undefined} @ http://localhost:5183/src/lib/components/copilot/chat/AIChatManager.svelte.ts?t=1772535251800:1024
[LOG] loadFlow @ http://localhost:5183/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte?t=1772543608757:100
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [a-end], a(module) ← [Input], a-end(branchOneEnd) ← [a-branch-default], a-branch-default(branchOneStart) ← [a]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
},
{
"id": "a-end",
"width": 275,
"height": 34
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "RIGHT",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "a-branch-default",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [],
"edges": []
}
]
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
]
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
]
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543608757:532
+674
View File
@@ -0,0 +1,674 @@
Total messages: 21 (Errors: 0, Warnings: 1)
Returning 10 messages for level "info"
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [Input]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543797839:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-Input-Result",
"sources": [
"Input"
],
"targets": [
"Result"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543797839:532
[LOG] cancelling request: {reason: saveAndClear, abortController: undefined} @ http://localhost:5183/src/lib/components/copilot/chat/AIChatManager.svelte.ts?t=1772535251800:1024
[LOG] loadFlow @ http://localhost:5183/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte?t=1772543797839:100
[LOG] Welcome back admin to testing @ http://localhost:5183/src/routes/(root)/+layout.svelte?t=1772535251800:68
[LOG] ELK result: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34,
"$H": 277,
"x": 12,
"y": 12
},
{
"id": "Input",
"width": 275,
"height": 34,
"$H": 279,
"x": 12,
"y": 108
},
{
"id": "Result",
"width": 275,
"height": 34,
"$H": 281,
"x": 12,
"y": 204
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
],
"sections": [
{
"id": "e-Trigger-Input_s0",
"startPoint": {
"x": 150,
"y": 46
},
"endPoint": {
"x": 150,
"y": 108
},
"incomingShape": "Trigger",
"outgoingShape": "Input"
}
],
"container": "root"
},
{
"id": "e-Input-Result",
"sources": [
"Input"
],
"targets": [
"Result"
],
"sections": [
{
"id": "e-Input-Result_s0",
"startPoint": {
"x": 150,
"y": 142
},
"endPoint": {
"x": 150,
"y": 204
},
"incomingShape": "Input",
"outgoingShape": "Result"
}
],
"container": "root"
}
],
"$H": 13,
"x": 0,
"y": 0,
"width": 299,
"height": 250
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543797839:536
[WARNING] using deprecated parameters for the initialization function; pass a single object instead @ http://localhost:5183/node_modules/@sveltejs/kit/src/runtime/client/client.js?v=5452fe86:3211
[LOG] ELK input nodes: [Trigger(trigger) ← [], Input(input2) ← [Trigger], Result(result) ← [a-end], a(module) ← [Input], a-end(branchAllEnd) ← [c,d,g], a-branch-0(branchAllStart) ← [a], b(module) ← [a-branch-0], c(module) ← [b], a-branch-1(branchAllStart) ← [a], d(module) ← [a-branch-1], a-branch-2(branchAllStart) ← [a], e(module) ← [a-branch-2], f(module) ← [e], g(module) ← [f]] @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543797839:531
[LOG] ELK graph: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34
},
{
"id": "Input",
"width": 275,
"height": 34
},
{
"id": "Result",
"width": 275,
"height": 34
},
{
"id": "a-end",
"width": 275,
"height": 34
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "a-branch-0",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "b",
"width": 275,
"height": 34
},
{
"id": "c",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-b-c",
"sources": [
"b"
],
"targets": [
"c"
]
}
]
},
{
"id": "a-branch-1",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "d",
"width": 275,
"height": 34
}
],
"edges": []
},
{
"id": "a-branch-2",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "e",
"width": 275,
"height": 34
},
{
"id": "f",
"width": 275,
"height": 34
},
{
"id": "g",
"width": 275,
"height": 34
}
],
"edges": [
{
"id": "e-e-f",
"sources": [
"e"
],
"targets": [
"f"
]
},
{
"id": "e-f-g",
"sources": [
"f"
],
"targets": [
"g"
]
}
]
}
]
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
]
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
]
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
]
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
]
}
]
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543797839:532
[LOG] ELK result: {
"id": "root",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50",
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
"elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
},
"children": [
{
"id": "Trigger",
"width": 275,
"height": 34,
"$H": 352,
"x": 36,
"y": 12
},
{
"id": "Input",
"width": 275,
"height": 34,
"$H": 354,
"x": 36,
"y": 108
},
{
"id": "Result",
"width": 275,
"height": 34,
"$H": 356,
"x": 36,
"y": 888
},
{
"id": "a-end",
"width": 275,
"height": 34,
"$H": 358,
"x": 36,
"y": 792
},
{
"id": "a",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "a-branch-0",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "b",
"width": 275,
"height": 34,
"$H": 364,
"x": 12,
"y": 12
},
{
"id": "c",
"width": 275,
"height": 34,
"$H": 366,
"x": 12,
"y": 108
}
],
"edges": [
{
"id": "e-b-c",
"sources": [
"b"
],
"targets": [
"c"
],
"sections": [
{
"id": "e-b-c_s0",
"startPoint": {
"x": 149.5,
"y": 46
},
"endPoint": {
"x": 149.5,
"y": 108
},
"incomingShape": "b",
"outgoingShape": "c"
}
],
"container": "a-branch-0"
}
],
"$H": 362,
"x": 12,
"y": 90,
"width": 299,
"height": 154
},
{
"id": "a-branch-1",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "d",
"width": 275,
"height": 34,
"$H": 370,
"x": 12,
"y": 12
}
],
"edges": [],
"$H": 368,
"x": 12,
"y": 12,
"width": 299,
"height": 58
},
{
"id": "a-branch-2",
"layoutOptions": {
"elk.algorithm": "layered",
"elk.direction": "DOWN",
"elk.layered.spacing.nodeNodeBetweenLayers": "62",
"elk.spacing.nodeNode": "50"
},
"children": [
{
"id": "e",
"width": 275,
"height": 34,
"$H": 374,
"x": 12,
"y": 12
},
{
"id": "f",
"width": 275,
"height": 34,
"$H": 376,
"x": 12,
"y": 108
},
{
"id": "g",
"width": 275,
"height": 34,
"$H": 378,
"x": 12,
"y": 204
}
],
"edges": [
{
"id": "e-e-f",
"sources": [
"e"
],
"targets": [
"f"
],
"sections": [
{
"id": "e-e-f_s0",
"startPoint": {
"x": 149.5,
"y": 46
},
"endPoint": {
"x": 149.5,
"y": 108
},
"incomingShape": "e",
"outgoingShape": "f"
}
],
"container": "a-branch-2"
},
{
"id": "e-f-g",
"sources": [
"f"
],
"targets": [
"g"
],
"sections": [
{
"id": "e-f-g_s0",
"startPoint": {
"x": 149.5,
"y": 142
},
"endPoint": {
"x": 149.5,
"y": 204
},
"incomingShape": "f",
"outgoingShape": "g"
}
],
"container": "a-branch-2"
}
],
"$H": 372,
"x": 12,
"y": 264,
"width": 299,
"height": 250
}
],
"$H": 360,
"x": 12,
"y": 204,
"width": 323,
"height": 526
}
],
"edges": [
{
"id": "e-Trigger-Input",
"sources": [
"Trigger"
],
"targets": [
"Input"
],
"sections": [
{
"id": "e-Trigger-Input_s0",
"startPoint": {
"x": 174,
"y": 46
},
"endPoint": {
"x": 174,
"y": 108
},
"incomingShape": "Trigger",
"outgoingShape": "Input"
}
],
"container": "root"
},
{
"id": "e-a-end-Result",
"sources": [
"a-end"
],
"targets": [
"Result"
],
"sections": [
{
"id": "e-a-end-Result_s0",
"startPoint": {
"x": 174,
"y": 826
},
"endPoint": {
"x": 174,
"y": 888
},
"incomingShape": "a-end",
"outgoingShape": "Result"
}
],
"container": "root"
},
{
"id": "e-Input-a",
"sources": [
"Input"
],
"targets": [
"a"
],
"sections": [
{
"id": "e-Input-a_s0",
"startPoint": {
"x": 174,
"y": 142
},
"endPoint": {
"x": 174,
"y": 204
},
"incomingShape": "Input",
"outgoingShape": "a"
}
],
"container": "root"
},
{
"id": "e-a-a-end",
"sources": [
"a"
],
"targets": [
"a-end"
],
"sections": [
{
"id": "e-a-a-end_s0",
"startPoint": {
"x": 174,
"y": 730
},
"endPoint": {
"x": 174,
"y": 792
},
"incomingShape": "a",
"outgoingShape": "a-end"
}
],
"container": "root"
}
],
"$H": 350,
"x": 0,
"y": 0,
"width": 347,
"height": 934
} @ http://localhost:5183/src/lib/components/graph/FlowGraphV2.svelte?t=1772543797839:536
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.648.0",
"version": "1.649.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.648.0",
"version": "1.649.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.648.0",
"version": "1.649.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -6,27 +6,20 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import Select from './select/Select.svelte'
import { ArrowLeft, Expand, LoaderCircle, Minimize, RefreshCcw } from 'lucide-svelte'
import type { DbInput } from './dbTypes'
import DBManagerContent from './DBManagerContent.svelte'
import { resource } from 'runed'
import { untrack } from 'svelte'
import type { DbManagerUriState } from './dbManagerDrawerModel.svelte'
interface Props {
uriState: DbManagerUriState
/** Z-index offset for the drawer, useful when opening from within modals */
offset?: number
}
let { offset = 0 }: Props = $props()
let { uriState, offset = 0 }: Props = $props()
let input: DbInput | undefined = $state()
let open = $derived(!!input)
// For datatable inputs, track the selected datatable separately
let selectedDatatable = $state<string | undefined>(undefined)
// Check if input is a datatable type
const isDatatableInput = $derived(
input?.type === 'database' && input.resourcePath.startsWith('datatable://')
)
let open = $derived(uriState.open)
// Load available datatables when drawer opens with datatable input
const datatables = resource<string[]>([], async () => {
@@ -39,16 +32,6 @@
}
})
// Computed input that updates when selectedDatatable changes
const effectiveInput: DbInput | undefined = $derived.by(() => {
if (!input) return undefined
if (!isDatatableInput || !selectedDatatable) return input
return {
...input,
resourcePath: `datatable://${selectedDatatable}`
}
})
const datatableItems = $derived(
datatables.current.map((dt) => ({
value: dt,
@@ -56,32 +39,26 @@
}))
)
export function openDrawer(nInput: DbInput) {
input = nInput
if (isDatatableInput) {
datatables.refetch()
// Refetch datatables when switching to a datatable input
$effect(() => {
if (uriState.isDatatableInput) {
untrack(() => datatables.refetch())
}
// If it's a datatable input, extract the datatable name for the selector
if (nInput.type === 'database' && nInput.resourcePath.startsWith('datatable://')) {
selectedDatatable = nInput.resourcePath.replace('datatable://', '')
datatables.refetch()
} else {
selectedDatatable = undefined
}
}
export function closeDrawer() {
input = undefined
selectedDatatable = undefined
})
function handleClose() {
uriState.closeDrawer()
dbManagerContent?.clearReplResult()
if (window.location.hash.startsWith('#dbmanager:'))
history.replaceState('', document.title, window.location.href.replace(/#dbmanager:.*$/, ''))
}
let windowWidth = $state(window.innerWidth)
let expand = $state(false)
$effect(() => {
if (!open) expand = false
if (!open) {
expand = false
uriState.closeDrawer()
}
})
let dbManagerContent: DBManagerContent | undefined = $state()
@@ -96,7 +73,7 @@
size={expand ? `${windowWidth}px` : '1200px'}
preventEscape
{offset}
on:close={closeDrawer}
on:close={handleClose}
>
<DrawerContent
title={hasReplResult ? 'Query Result' : 'Database Manager'}
@@ -104,18 +81,24 @@
if (hasReplResult) {
dbManagerContent?.clearReplResult()
} else {
closeDrawer()
handleClose()
}
}}
CloseIcon={hasReplResult ? ArrowLeft : undefined}
noPadding
id="db-manager-drawer"
>
{#if effectiveInput && $workspaceStore}
{#key selectedDatatable}
<DBManagerContent bind:this={dbManagerContent} input={effectiveInput} bind:hasReplResult>
{#if uriState.effectiveInput && $workspaceStore}
{#key uriState.selectedDatatable}
<DBManagerContent
bind:this={dbManagerContent}
input={uriState.effectiveInput}
bind:hasReplResult
bind:selectedSchemaKey={uriState.selectedSchema}
bind:selectedTableKey={uriState.selectedTable}
>
{#snippet dbSelector()}
{#if isDatatableInput}
{#if uriState.isDatatableInput}
{#if datatables.loading}
<div class="flex items-center gap-2 text-tertiary ml-2">
<LoaderCircle size={14} class="animate-spin" />
@@ -125,7 +108,7 @@
<Select
transformInputSelectedText={(s) => `Datatable: ${s}`}
items={datatableItems}
bind:value={selectedDatatable}
bind:value={uriState.selectedDatatable}
placeholder="Select data table"
size="md"
/>
@@ -1,6 +1,6 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import Select from './select/Select.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
@@ -31,7 +31,7 @@
let datatables = usePromise(() =>
WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
)
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
</script>
<div class={className}>
@@ -49,7 +49,6 @@
<ExploreAssetButton
class="mt-1 w-fit"
asset={{ kind: 'datatable', path: value }}
{dbManagerDrawer}
/>
{/if}
</div>
@@ -62,6 +62,9 @@
{item.displayName}
</p>
{@render item.extra?.()}
{#if item.shortcut}
<span class="ml-auto pl-4 text-2xs text-secondary shrink-0">{item.shortcut}</span>
{/if}
{#if item.tooltip}
<Tooltip>
{#snippet text()}
@@ -1,6 +1,6 @@
<script lang="ts">
import { WorkspaceService } from '$lib/gen'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import Select from './select/Select.svelte'
import ExploreAssetButton, { assetCanBeExplored } from './ExploreAssetButton.svelte'
@@ -31,7 +31,7 @@
let ducklakes = usePromise(() =>
WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? '' })
)
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
</script>
<div class={className}>
@@ -49,7 +49,6 @@
<ExploreAssetButton
class="mt-1 w-fit"
asset={{ kind: 'ducklake', path: value }}
{dbManagerDrawer}
/>
{/if}
</div>
@@ -16,9 +16,8 @@
import { isDbType } from '$lib/components/dbTypes'
import { formatAsset, type Asset } from '$lib/components/assets/lib'
import { Button, ButtonType } from '$lib/components/common'
import DbManagerDrawer from '$lib/components/DBManagerDrawer.svelte'
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import { userStore } from '$lib/stores'
import { globalDbManagerDrawer, userStore } from '$lib/stores'
import { isS3Uri } from '$lib/utils'
import { Database, File } from 'lucide-svelte'
import DucklakeIcon from './icons/DucklakeIcon.svelte'
@@ -27,7 +26,6 @@
asset,
_resourceMetadata,
s3FilePicker,
dbManagerDrawer,
onClick,
class: className = '',
noText = false,
@@ -38,7 +36,6 @@
asset: Asset
_resourceMetadata?: { resource_type?: string }
s3FilePicker?: S3FilePicker
dbManagerDrawer?: DbManagerDrawer
onClick?: () => void
class?: string
noText?: boolean
@@ -46,6 +43,8 @@
btnClasses?: string
disabled?: boolean
} = $props()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
const assetUri = $derived(formatAsset(asset))
</script>
+2 -10
View File
@@ -862,14 +862,6 @@
const mod = isMac() ? '⌘' : 'Ctrl+'
const undoShortcutSnippet = createRawSnippet(() => ({
render: () => `<span class="ml-auto text-2xs text-tertiary">${mod}Z</span>`
}))
const redoShortcutSnippet = createRawSnippet(() => ({
render: () => `<span class="ml-auto text-2xs text-tertiary">${mod}⇧Z</span>`
}))
function getMoreItems(): Item[] {
return [
...baseMenuItems,
@@ -878,7 +870,7 @@
icon: Undo,
action: () => handleUndo(),
disabled: $history.index === 0,
extra: undoShortcutSnippet,
shortcut: `${mod}Z`,
separatorTop: baseMenuItems.length > 0
},
{
@@ -886,7 +878,7 @@
icon: Redo,
action: () => handleRedo(),
disabled: $history.index === $history.history.length - 1,
extra: redoShortcutSnippet
shortcut: `${mod}⇧Z`
},
{
displayName: 'Tutorials',
@@ -1,6 +1,6 @@
<script lang="ts">
import { ResourceService, WorkspaceService } from '$lib/gen'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import { onMount, untrack } from 'svelte'
import AppConnect from './AppConnectDrawer.svelte'
import ResourceEditorDrawer from './ResourceEditorDrawer.svelte'
@@ -174,7 +174,6 @@
let appConnect: AppConnect | undefined = $state()
let resourceEditor: ResourceEditorDrawer | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let hovering = $state(false)
let isDatatableSelected = $derived(value?.startsWith('datatable://') ?? false)
</script>
@@ -317,7 +316,6 @@
class="mt-1"
_resourceMetadata={{ resource_type: resourceType }}
asset={{ kind: 'resource', path: value }}
{dbManagerDrawer}
/>
{/if}
</div>
+3 -1
View File
@@ -148,7 +148,9 @@
(v) => {
v.maxTs ? (filters.val.max_ts = new Date(v.maxTs)) : delete filters.val.max_ts
v.minTs ? (filters.val.min_ts = new Date(v.minTs)) : delete filters.val.min_ts
v.timeframe ? (filters.val.timeframe = v.timeframe) : delete filters.val.timeframe
v.timeframe && v.timeframe !== 'Latest runs'
? (filters.val.timeframe = v.timeframe)
: delete filters.val.timeframe
}
)
let timeframe = $derived(_timeframe.val)
@@ -8,7 +8,6 @@
type Props = {
s3FilePicker?: any | undefined
dbManagerDrawer?: any | undefined
resourceEditorDrawer?: ResourceEditorDrawer | undefined
resourceDataCache: Record<string, string | undefined>
asset: Asset
@@ -18,7 +17,6 @@
}
let {
s3FilePicker,
dbManagerDrawer,
resourceEditorDrawer,
resourceDataCache,
asset,
@@ -71,7 +69,6 @@
<ExploreAssetButton
{asset}
{s3FilePicker}
{dbManagerDrawer}
onClick={() => onClick?.()}
noText
_resourceMetadata={{ resource_type: resourceDataCacheValue }}
@@ -14,7 +14,7 @@
} from './lib'
import { untrack } from 'svelte'
import { ResourceService, WorkspaceService } from '$lib/gen'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import Tooltip from '../meltComponents/Tooltip.svelte'
import Tooltip2 from '../Tooltip.svelte'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
@@ -48,7 +48,6 @@
let blueBgDiv: HTMLDivElement | undefined = $state()
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
let isOpen = $state(false)
let resourceDataCache: Record<string, string | undefined> = $state({})
@@ -209,7 +208,6 @@
onClick={() => (isOpen = false)}
{asset}
{resourceDataCache}
{dbManagerDrawer}
{resourceEditorDrawer}
{s3FilePicker}
{ducklakeNotFound}
@@ -1,7 +1,7 @@
<script lang="ts">
import { ResourceService, type Job } from '$lib/gen'
import { inferAssets } from '$lib/infer'
import { globalDbManagerDrawer, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { pruneNullishArray, uniqueBy } from '$lib/utils'
import ResourceEditorDrawer from '../ResourceEditorDrawer.svelte'
@@ -64,7 +64,6 @@
})
let s3FilePicker: S3FilePicker | undefined = $state()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let resourceEditorDrawer: ResourceEditorDrawer | undefined = $state()
</script>
@@ -86,7 +85,6 @@
<AssetButtons
{asset}
{resourceDataCache}
{dbManagerDrawer}
{resourceEditorDrawer}
{s3FilePicker}
/>
@@ -0,0 +1,207 @@
import { z } from 'zod'
import { useSearchParams } from '$lib/svelte5UtilsKit.svelte'
import type { DbInput, DbType } from './dbTypes'
import { isDbType } from './dbTypes'
/**
* Single URL param `dbm` encodes the full DB manager state:
* firstSegment~path~schema.table
*
* firstSegment:
* datatable – database with datatable:// resource (resourceType always postgresql)
* ducklake – ducklake connection
* postgresql, mysql, … – regular database (the segment IS the resource type)
*
* schema.table (third segment, optional):
* schema.table – both
* .table – table only
* schema. – schema only
* (omitted) – neither
*
* Default schemas (omitted from URL, restored on parse):
* datatable → public
* ducklake → main
*
* Examples:
* datatable~main~.customers (schema "public" implied)
* ducklake~main~.orders (schema "main" implied)
* postgresql~$res:u/user/my_pg~public.customers
*/
const dbManagerSchema = z.object({
dbm: z.string().nullable()
})
interface ParsedDbm {
type: 'database' | 'datatable' | 'ducklake'
path: string
resType?: string
schema?: string
table?: string
}
function parseDbm(raw: unknown): ParsedDbm | null {
if (!raw || typeof raw !== 'string') return null
const parts = raw.split('~')
if (parts.length < 2 || !parts[1]) return null
const firstSeg = parts[0]
const path = parts[1]
const schemaTable = parts[2] ?? ''
let type: ParsedDbm['type']
let resType: string | undefined
if (firstSeg === 'datatable') {
type = 'datatable'
} else if (firstSeg === 'ducklake') {
type = 'ducklake'
} else if (isDbType(firstSeg)) {
type = 'database'
resType = firstSeg
} else {
return null
}
let schema: string | undefined
let table: string | undefined
if (schemaTable) {
const dotIdx = schemaTable.indexOf('.')
if (dotIdx === 0) {
table = schemaTable.slice(1) || undefined
} else if (dotIdx === schemaTable.length - 1) {
schema = schemaTable.slice(0, -1) || undefined
} else if (dotIdx > 0) {
schema = schemaTable.slice(0, dotIdx)
table = schemaTable.slice(dotIdx + 1)
}
}
// Restore default schema when omitted for datatable/ducklake
if (!schema && table && type in defaultSchemas) {
schema = defaultSchemas[type]
}
return { type, path, resType, schema, table }
}
const defaultSchemas: Record<string, string> = { datatable: 'public', ducklake: 'main' }
function buildDbm(p: ParsedDbm): string {
const firstSeg = p.type === 'database' ? p.resType! : p.type
const schema = p.schema === defaultSchemas[p.type] ? undefined : p.schema
let schemaTable = ''
if (schema && p.table) {
schemaTable = `${schema}.${p.table}`
} else if (p.table) {
schemaTable = `.${p.table}`
} else if (schema) {
schemaTable = `${schema}.`
}
return schemaTable ? `${firstSeg}~${p.path}~${schemaTable}` : `${firstSeg}~${p.path}`
}
export interface DbManagerUriState {
readonly input: DbInput | undefined
readonly effectiveInput: DbInput | undefined
readonly isDatatableInput: boolean
selectedDatatable: string | undefined
selectedSchema: string | undefined
selectedTable: string | undefined
readonly open: boolean
openDrawer: (nInput: DbInput) => void
closeDrawer: () => void
}
export function useDbManagerUriState(): DbManagerUriState {
const params = useSearchParams(dbManagerSchema)
const parsed = $derived(parseDbm(params.dbm))
let input: DbInput | undefined = $derived.by(() => {
if (!parsed) return undefined
if (parsed.type === 'ducklake') {
return {
type: 'ducklake' as const,
ducklake: parsed.path,
specificTable: parsed.table
}
}
// datatable or database
const resType = parsed.type === 'datatable' ? 'postgresql' : parsed.resType
if (!isDbType(resType ?? undefined)) return undefined
return {
type: 'database' as const,
resourceType: resType as DbType,
resourcePath: parsed.type === 'datatable' ? `datatable://${parsed.path}` : parsed.path,
specificSchema: parsed.schema,
specificTable: parsed.table
}
})
const isDatatableInput = $derived(parsed?.type === 'datatable')
function updateField(updates: Partial<ParsedDbm>) {
const p = parseDbm(params.dbm)
if (!p) return
Object.assign(p, updates)
params.dbm = buildDbm(p)
}
function openDrawer(nInput: DbInput) {
if (nInput.type === 'database') {
const isDatatable = nInput.resourcePath.startsWith('datatable://')
params.dbm = buildDbm({
type: isDatatable ? 'datatable' : 'database',
path: isDatatable ? nInput.resourcePath.slice('datatable://'.length) : nInput.resourcePath,
resType: isDatatable ? undefined : nInput.resourceType,
schema: nInput.specificSchema,
table: nInput.specificTable
})
} else {
params.dbm = buildDbm({
type: 'ducklake',
path: nInput.ducklake,
table: nInput.specificTable
})
}
}
function closeDrawer() {
params.dbm = null
}
return {
get input() {
return input
},
get effectiveInput() {
return input
},
get isDatatableInput() {
return isDatatableInput
},
get selectedDatatable() {
return parsed?.type === 'datatable' ? parsed.path : undefined
},
set selectedDatatable(v: string | undefined) {
if (v) updateField({ path: v })
},
get selectedSchema() {
return parsed?.schema
},
set selectedSchema(v: string | undefined) {
updateField({ schema: v })
},
get selectedTable() {
return parsed?.table
},
set selectedTable(v: string | undefined) {
updateField({ table: v })
},
get open() {
return !!input
},
openDrawer,
closeDrawer
}
}
@@ -16,6 +16,11 @@
import FlowResult from './FlowResult.svelte'
import type { StateStore } from '$lib/utils'
import FlowSelectionPanel from './FlowSelectionPanel.svelte'
import {
resolveSelectedModuleIds,
locateModules,
areContiguousSiblings
} from '../multiSelectUtils'
interface Props {
noEditor?: boolean
@@ -89,10 +94,29 @@
$effect(() => {
computeMissingInputWarnings(flowStore, flowStateStore.val, flowInputsStore)
})
// Derived state for multi-select operations in the side panel
let resolvedModuleIds = $derived(
resolveSelectedModuleIds(selectionManager.selectedIds, flowStore.val.value.modules ?? [])
)
let canMoveSelected = $derived(
resolvedModuleIds.length > 0 &&
areContiguousSiblings(
locateModules(resolvedModuleIds, flowStore.val.value.modules ?? [])
)
)
</script>
{#if selectionManager && selectionManager.selectedIds.length > 1}
<FlowSelectionPanel {selectionManager} {noEditor} />
<FlowSelectionPanel
{selectionManager}
{noEditor}
onDeleteSelected={() => flowModuleSchemaMap?.deleteMultiple(resolvedModuleIds)}
onDuplicateSelected={() => flowModuleSchemaMap?.duplicateMultiple(resolvedModuleIds)}
onMoveSelected={() => flowModuleSchemaMap?.moveMultiple(resolvedModuleIds)}
{canMoveSelected}
resolvedCount={resolvedModuleIds.length}
/>
{:else if selectedId?.startsWith('settings')}
<FlowSettings {enableAi} {noEditor} />
{:else if selectedId === 'Input'}
@@ -425,7 +425,6 @@
on:blur={() => {
iteratorFieldFocused = false
}}
autofocus
lang="javascript"
bind:code={mod.value.iterator.expr}
class="h-full"
@@ -2,14 +2,29 @@
import FlowCard from '../common/FlowCard.svelte'
import type { SelectionManager } from '$lib/components/graph/selectionUtils.svelte'
import { Button } from '$lib/components/common'
import DropdownV2 from '$lib/components/DropdownV2.svelte'
import { getNoteEditorContext } from '$lib/components/graph/noteEditor.svelte'
import { StickyNote } from 'lucide-svelte'
import { StickyNote, Move, Copy, Trash2 } from 'lucide-svelte'
import type { Item } from '$lib/utils'
interface Props {
selectionManager: SelectionManager
noEditor: boolean
onDeleteSelected?: () => void
onDuplicateSelected?: () => void
onMoveSelected?: () => void
canMoveSelected?: boolean
resolvedCount?: number
}
let { selectionManager, noEditor }: Props = $props()
let {
selectionManager,
noEditor,
onDeleteSelected,
onDuplicateSelected,
onMoveSelected,
canMoveSelected = false,
resolvedCount = 0
}: Props = $props()
const noteEditorContext = getNoteEditorContext()
@@ -19,20 +34,44 @@
noteEditorContext.noteEditor.createGroupNote(selectionManager.selectedIds)
}
}
let menuItems: Item[] = $derived([
{
displayName: 'Move',
icon: Move,
action: () => onMoveSelected?.(),
disabled: !canMoveSelected
},
{
displayName: 'Duplicate',
icon: Copy,
action: () => onDuplicateSelected?.()
},
{
displayName: `Delete (${resolvedCount})`,
icon: Trash2,
type: 'delete',
action: () => onDeleteSelected?.()
}
])
</script>
<FlowCard {noEditor} title="Multiple Selection">
{#snippet action()}
<Button
onClick={addGroupNote}
disabled={!noteEditorContext?.noteEditor || selectionManager.selectedIds.length === 0}
startIcon={{ icon: StickyNote }}
>
Create group note
</Button>
<div class="flex gap-1 items-center">
<Button
onClick={addGroupNote}
disabled={!noteEditorContext?.noteEditor || selectionManager.selectedIds.length === 0}
startIcon={{ icon: StickyNote }}
>
Create group note
</Button>
{#if resolvedCount > 0}
<DropdownV2 items={menuItems} />
{/if}
</div>
{/snippet}
<div class="px-4">
<p class="text-xs text-secondary mb-4">{selectionManager.selectedIds.length} nodes selected</p>
<div class="space-y-2 mb-4">
{#each selectionManager.selectedIds as nodeId}
<div class="text-sm px-2 py-1 bg-surface rounded border">
@@ -17,3 +17,15 @@ export function nextId(flowState: FlowState, fullFlow: OpenFlow): string {
}, 0)
return numberToChars(max)
}
// Computes a copy id like "a2", "a3", etc. based on the original id
export function copyId(originalId: string, flowState: FlowState, fullFlow: OpenFlow): string {
const allIds = new Set(dfs(fullFlow.value.modules, (fm) => fm.id).concat(Object.keys(flowState)))
for (let n = 2; n < 10000; n++) {
const candidate = `${originalId}${n}`
if (!allIds.has(candidate)) {
return candidate
}
}
return `${originalId}10000`
}
@@ -2,12 +2,13 @@
import { preventDefault, stopPropagation } from 'svelte/legacy'
import Popover from '$lib/components/Popover.svelte'
import { classNames, type StateStore } from '$lib/utils'
import DropdownV2 from '$lib/components/DropdownV2.svelte'
import { classNames, type Item, type StateStore } from '$lib/utils'
import {
Bed,
Database,
Gauge,
Move,
EllipsisVertical,
PhoneIncoming,
Repeat,
Square,
@@ -20,7 +21,7 @@
Timer,
Maximize2
} from 'lucide-svelte'
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { fade } from 'svelte/transition'
import type { FlowEditorContext } from '../types'
import { twMerge } from 'tailwind-merge'
@@ -48,6 +49,7 @@
import type { ModuleActionInfo } from '$lib/components/flows/flowDiff'
import DiffActionBar from './DiffActionBar.svelte'
import { getGraphContext } from '$lib/components/graph/graphContext'
import MoveHandleButton from '$lib/components/graph/MoveHandleButton.svelte'
interface Props {
selected?: boolean
@@ -69,7 +71,6 @@
id?: string | undefined
label: string
path?: string
modType?: string | undefined
nodeState?: FlowNodeState
concurrency?: boolean
// TODO: Implement for this one. See how concurrency is implemented.
@@ -89,6 +90,7 @@
isOwner?: boolean
enableTestRun?: boolean
maximizeSubflow?: () => void
menuItems?: Item[]
}
let {
@@ -106,7 +108,6 @@
id = undefined,
label,
path = '',
modType = undefined,
nodeState,
concurrency = false,
debouncing = false,
@@ -123,7 +124,8 @@
onEditInput,
flowJob,
enableTestRun = false,
maximizeSubflow = undefined
maximizeSubflow = undefined,
menuItems = undefined
}: Props = $props()
// AI action colors take priority over execution state
@@ -138,6 +140,9 @@
const diffManager = flowGraphContext?.diffManager
const moveManager = flowGraphContext?.moveManager
// Hide per-node action buttons when multiple nodes are selected (multi-select mode)
let isMultiSelected = $derived((flowGraphContext?.selectionManager?.selectedIds?.length ?? 0) > 1)
let pickableIds: Record<string, any> | undefined = $state(undefined)
const dispatch = createEventDispatcher()
@@ -161,6 +166,7 @@
let outputPicker: OutputPicker | undefined = $state(undefined)
let testJob: any | undefined = $state(undefined)
let outputPickerBarOpen = $state(false)
let dropdownOpen = $state(false)
let flowStateStore = $derived(flowEditorContext?.flowStateStore)
@@ -188,42 +194,6 @@
let isDragging = $derived(!!moveManager?.dragging)
// --- Drag handle logic ---
let dragCleanup: (() => void) | undefined
function onMovePointerDown(e: Event) {
const pe = e as PointerEvent
const startX = pe.clientX
const startY = pe.clientY
let didDrag = false
function onMovePointer(me: PointerEvent) {
const dx = me.clientX - startX
const dy = me.clientY - startY
if (!didDrag && Math.sqrt(dx * dx + dy * dy) > 5) {
didDrag = true
if (moveManager && id) {
moveManager.startDrag(id, startX, startY)
}
}
}
function onUp() {
document.removeEventListener('pointermove', onMovePointer)
document.removeEventListener('pointerup', onUp)
dragCleanup = undefined
if (!didDrag) {
dispatch('move')
}
}
document.addEventListener('pointermove', onMovePointer)
document.addEventListener('pointerup', onUp)
dragCleanup = onUp
}
onDestroy(() => dragCleanup?.())
const outputPickerVisible = $derived(
editMode && (isConnectingCandidate || alwaysShowOutputPicker) && !!id && !isDragging
)
@@ -467,6 +437,7 @@
{deletable}
{bold}
bind:editId
disableEditId={isMultiSelected}
{hover}
{colorClasses}
>
@@ -475,7 +446,7 @@
{/snippet}
</FlowModuleSchemaItemViewer>
{#if outputPickerVisible}
{#if outputPickerVisible && !isMultiSelected}
<OutputPicker
bind:this={outputPicker}
{selected}
@@ -518,50 +489,6 @@
{@render buttonMaximizeSubflow?.()}
{/if}
{#if id !== 'preprocessor'}
<!-- The `style="will-change: transform;"` fixes a bug in Safari where the close and move
and delete buttons would get clipped (unless an animation is running) -->
<div
class={twMerge('absolute -translate-y-[100%] top-2 right-4 h-7 p-1 min-w-7')}
style="will-change: transform;"
>
<button
class={twMerge(
'trash center-center p-1 text-secondary shadow-sm bg-surface duration-0 hover:bg-surface-tertiary cursor-grab',
hover || selected ? 'block' : '!hidden',
'shadow-md rounded-md',
'group-hover:block',
'touch-none'
)}
onpointerdown={stopPropagation(preventDefault(onMovePointerDown))}
title="Move"
>
<Move size={12} />
</button>
</div>
{/if}
<div
class="absolute -translate-y-[100%] top-2 -right-2 h-7 p-1 min-w-7"
style="will-change: transform;"
>
<button
class={twMerge(
'trash center-center text-secondary shadow-sm bg-surface duration-0 hover:bg-red-400 hover:text-white p-1',
selected || hover ? 'block' : '!hidden',
'group-hover:block',
'shadow-md rounded-md'
)}
title="Delete"
onclick={stopPropagation(
preventDefault((event) => dispatch('delete', { id, type: modType }))
)}
onpointerdown={stopPropagation(preventDefault(() => {}))}
>
<X size={12} />
</button>
</div>
{#if (id && Object.values($flowInputsStore?.[id]?.flowStepWarnings || {}).length > 0) || Boolean(warningMessage)}
<Popover
style="will-change: transform;"
@@ -591,6 +518,52 @@
<TriangleAlert size={12} strokeWidth={2} />
</Popover>
{/if}
{#if !isMultiSelected && id !== 'preprocessor' && moveManager && id}
<div
class="absolute -translate-y-[100%] top-2 right-5 h-7 p-1 min-w-7"
style="will-change: transform;"
>
<MoveHandleButton
{moveManager}
moduleId={id}
singleNode
visible={hover || selected || dropdownOpen}
onClickMove={() => dispatch('move')}
class="trash group-hover:block"
/>
</div>
{/if}
{#if !isMultiSelected && id !== 'preprocessor' && menuItems && menuItems.length > 0}
<div
class="absolute -translate-y-[100%] top-2 -right-2 h-7 p-1 min-w-7"
style="will-change: transform;"
>
<DropdownV2
items={menuItems}
placement="bottom-end"
bind:open={dropdownOpen}
fixedHeight={false}
usePointerDownOutside
>
{#snippet buttonReplacement()}
<button
class={twMerge(
'trash center-center p-1 text-secondary shadow-sm bg-surface duration-0 hover:bg-surface-tertiary',
hover || selected || dropdownOpen ? 'block' : '!hidden',
'shadow-md rounded-md',
'group-hover:block'
)}
onpointerdown={stopPropagation(preventDefault(() => {}))}
title="Actions"
>
<EllipsisVertical size={12} />
</button>
{/snippet}
</DropdownV2>
</div>
{/if}
{:else if maximizeSubflow !== undefined}
{@render buttonMaximizeSubflow?.()}
{/if}
@@ -603,7 +576,7 @@
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
>
{#if (hover || selected || testRunDropdownOpen) && outputPickerVisible}
{#if !isMultiSelected && (hover || selected || testRunDropdownOpen) && outputPickerVisible}
<div transition:fade={{ duration: 100 }}>
{#if !testIsLoading}
<Button
@@ -655,13 +628,13 @@
</div>
{#snippet buttonMaximizeSubflow()}
<div class="absolute -translate-y-[100%] top-2 right-10 h-7 p-1">
<div class="absolute -translate-y-[100%] top-2 right-12 h-7 p-1">
<button
title="Expand subflow"
class={twMerge(
'center-center text-secondary shadow-sm bg-surface duration-0 hover:bg-surface-tertiary p-1',
'shadow-md rounded-md',
hover || selected ? 'opacity-100' : 'opacity-50'
!isMultiSelected && (hover || selected) ? 'opacity-100' : 'opacity-50'
)}
onclick={(e) => {
e.stopPropagation()
@@ -15,6 +15,7 @@
deletable?: boolean
bold?: boolean
editId?: boolean
disableEditId?: boolean
hover?: boolean
colorClasses?: FlowNodeColorClasses
icon?: import('svelte').Snippet
@@ -28,6 +29,7 @@
deletable = false,
bold = false,
editId = $bindable(false),
disableEditId = false,
hover = false,
colorClasses,
icon,
@@ -74,16 +76,18 @@
)}
baseClass={twMerge('!px-1')}
title={id}
clickable
onclick={(e) => {
e?.preventDefault()
e?.stopPropagation()
editId = !editId
onclick?.()
}}
clickable={!disableEditId}
onclick={disableEditId
? undefined
: (e) => {
e?.preventDefault()
e?.stopPropagation()
editId = !editId
onclick?.()
}}
>
<span class="max-w-full text-2xs truncate flex items-center">
{#if editId || (hover && deletable)}
{#if !disableEditId && (editId || (hover && deletable))}
<span transition:slide={{ axis: 'x', duration: 100 }}>
<Pencil size={10} class="mr-1" />
</span>
@@ -18,11 +18,13 @@
import { emptyFlowModuleState } from '../utils.svelte'
import { dfs } from '../dfs'
import { nextId, copyId } from '../flowModuleNextId'
import { push } from '$lib/history.svelte'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import Portal from '$lib/components/Portal.svelte'
import { getDependentComponents } from '../flowExplorer'
import { locateModules, groupByParent } from '../multiSelectUtils'
import { workspaceStore } from '$lib/stores'
import { copilotInfo } from '$lib/aiStore'
import FlowTutorials from '$lib/components/FlowTutorials.svelte'
@@ -345,6 +347,80 @@
noteMode = !noteMode
}
export function deleteMultiple(ids: string[]) {
const deletingSet = new Set(ids)
const allDeps: Record<string, string[]> = {}
for (const id of ids) {
const deps = getDependentComponents(id, flowStore.val)
for (const [depId, exprs] of Object.entries(deps)) {
if (!deletingSet.has(depId)) {
allDeps[depId] = [...(allDeps[depId] ?? []), ...exprs]
}
}
}
const cb = () => {
push(history, flowStore.val)
for (const id of ids) {
removeAtId(flowStore.val.value.modules, id)
delete flowStateStore.val[id]
}
selectionManager.clearSelection()
refreshStateStore(flowStore)
}
if (Object.keys(allDeps).length > 0) {
dependents = allDeps
deleteCallback = cb
} else {
cb()
}
}
export function duplicateMultiple(ids: string[]) {
const locations = locateModules(ids, flowStore.val.value.modules)
const groups = groupByParent(locations)
push(history, flowStore.val)
const allCloneIds: string[] = []
for (const group of groups) {
const sorted = [...group].sort((a, b) => a.index - b.index)
const parentArr = sorted[0].parentArray
const lastIndex = sorted[sorted.length - 1].index
const clones: FlowModule[] = []
for (const loc of sorted) {
const original = parentArr[loc.index]
const clone: FlowModule = $state.snapshot(original)
clone.id = copyId(original.id, flowStateStore.val, flowStore.val)
flowStateStore.val[clone.id] = emptyFlowModuleState()
dfs([clone], (mod) => {
if (mod.id !== clone.id) {
const newModId = nextId(flowStateStore.val, flowStore.val)
mod.id = newModId
flowStateStore.val[newModId] = emptyFlowModuleState()
}
})
clones.push(clone)
allCloneIds.push(clone.id)
}
parentArr.splice(lastIndex + 1, 0, ...clones)
}
refreshStateStore(flowStore)
selectionManager.selectByIds(allCloneIds)
}
export function moveMultiple(ids: string[]) {
moveManager.toggleMovingMultiple(ids)
}
const dispatch = createEventDispatcher<{
generateStep: { moduleId: string; instructions: string; lang: ScriptLang }
change: void
@@ -533,18 +609,37 @@
if (flowStore.val.value.modules && Array.isArray(flowStore.val.value.modules)) {
await tick()
if (moveManager.movingModuleId) {
// console.log('modules', modules, movingModules, movingModule)
push(history, flowStore.val)
let indexToRemove = originalModules.findIndex((m) => moveManager.movingModuleId == m.id)
let [removedModule] = originalModules.splice(indexToRemove, 1)
// When moving within the same array, removal shifts subsequent indices down by 1
let insertIndex = detail.index
if (originalModules === targetModules && indexToRemove < detail.index) {
insertIndex -= 1
if (!originalModules || !targetModules) {
moveManager.clearMoving()
return
}
if (moveManager.movingIds && moveManager.movingIds.length > 1) {
// Multi-move: splice out all moving modules from their parent, insert at target
const firstIndex = originalModules.findIndex(
(m) => m.id === moveManager.movingIds?.[0]
)
const removedModules = originalModules.splice(
firstIndex,
moveManager.movingIds.length
)
let insertIndex = detail.index
if (originalModules === targetModules && firstIndex < detail.index) {
insertIndex -= moveManager.movingIds.length
}
targetModules.splice(insertIndex, 0, ...removedModules)
selectionManager.selectByIds(removedModules.map((m) => m.id))
} else {
let indexToRemove = originalModules.findIndex((m) => moveManager.movingModuleId == m.id)
let [removedModule] = originalModules.splice(indexToRemove, 1)
// When moving within the same array, removal shifts subsequent indices down by 1
let insertIndex = detail.index
if (originalModules === targetModules && indexToRemove < detail.index) {
insertIndex -= 1
}
targetModules.splice(insertIndex, 0, removedModule)
selectionManager.selectId(removedModule.id)
}
targetModules.splice(insertIndex, 0, removedModule)
selectionManager.selectId(removedModule.id)
moveManager.clearMoving()
} else {
if (detail.isPreprocessor) {
@@ -678,6 +773,41 @@
onMove={(id) => {
moveManager.toggleMoving(id)
}}
onDuplicate={(id) => {
let targetModules: FlowModule[] | undefined
let targetIndex: number = -1
dfs(flowStore.val.value.modules, (mod, modules) => {
const idx = modules.findIndex((m) => m.id === id)
if (idx !== -1) {
targetModules = modules
targetIndex = idx
}
})
if (!targetModules || targetIndex === -1) return
push(history, flowStore.val)
const original = targetModules[targetIndex]
const clone: FlowModule = $state.snapshot(original)
// Assign copy id to the clone, and fresh ids to nested modules
clone.id = copyId(original.id, flowStateStore.val, flowStore.val)
flowStateStore.val[clone.id] = emptyFlowModuleState()
dfs([clone], (mod) => {
if (mod.id !== clone.id) {
const newModId = nextId(flowStateStore.val, flowStore.val)
mod.id = newModId
flowStateStore.val[newModId] = emptyFlowModuleState()
}
})
targetModules.splice(targetIndex + 1, 0, clone)
refreshStateStore(flowStore)
selectionManager.selectId(clone.id)
}}
onUpdateMock={(detail) => {
let module = findModuleById(detail.id)
module.mock = $state.snapshot(detail.mock)
@@ -696,6 +826,10 @@
}
}}
multiSelectEnabled
movingIds={moveManager.movingIds}
onDeleteMultiple={deleteMultiple}
onDuplicateMultiple={duplicateMultiple}
onMoveMultiple={moveMultiple}
/>
</div>
</div>

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