Merge branch 'main' into instructions_helm

This commit is contained in:
hcourdent
2023-09-26 11:57:56 +02:00
100 changed files with 2090 additions and 1119 deletions
+3
View File
@@ -23,6 +23,8 @@ jobs:
env:
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
WORKER_GROUP: main
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow
options: >-
--pull always --health-interval 10s --health-timeout 5s
--health-retries 5 --health-cmd "curl
@@ -34,6 +36,7 @@ jobs:
env:
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
DISABLE_SERVER: true
WORKER_GROUP: dedicated
DEDICATED_WORKER: "admins:f/benchmarks/dedicated"
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
options: >-
+23
View File
@@ -1,6 +1,29 @@
# Changelog
## [1.176.0](https://github.com/windmill-labs/windmill/compare/v1.175.0...v1.176.0) (2023-09-24)
### Features
* add license key as superadmin setting ([#2321](https://github.com/windmill-labs/windmill/issues/2321)) ([304a259](https://github.com/windmill-labs/windmill/commit/304a2596fd29fbd9a79c5cf9fe4df7b44d5c5254))
* add running filter ([ea364ad](https://github.com/windmill-labs/windmill/commit/ea364ad9602647cbc9e8ee78fb5f17f0012105f6))
* ai flow trigger menu ([#2317](https://github.com/windmill-labs/windmill/issues/2317)) ([95194ab](https://github.com/windmill-labs/windmill/commit/95194abeacc42416174ee9dd79b75f2204a40d33))
* improved dedicated benchmarks + buffer fix ([#2313](https://github.com/windmill-labs/windmill/issues/2313)) ([fc93c2a](https://github.com/windmill-labs/windmill/commit/fc93c2a7cece95c00070a3a3391ae2bcb4513e85))
* set instance settings from UI ([#2314](https://github.com/windmill-labs/windmill/issues/2314)) ([2f0e43b](https://github.com/windmill-labs/windmill/commit/2f0e43bfdbd1e196131f126c83b1d7dd2eea98d8))
### Bug Fixes
* add ability to test this step for flow step ([3585929](https://github.com/windmill-labs/windmill/commit/3585929bb758b0cfc2cbe43f66597b184e7b8ee0))
* benchmark worker tags ([#2319](https://github.com/windmill-labs/windmill/issues/2319)) ([481bcd5](https://github.com/windmill-labs/windmill/commit/481bcd53cb07e4520d5fd81572cad74340c4eb64))
* change cache implementation to remove async-timer ([4911b4b](https://github.com/windmill-labs/windmill/commit/4911b4b3fd6e3a9f6bccc4c8712b736e18dcb6e1))
* fix upto preview issue with nested flows ([6492ff6](https://github.com/windmill-labs/windmill/commit/6492ff627a800832e12a31fd89a6070703988eb9))
* flow steps appears in all static inputs ([c043847](https://github.com/windmill-labs/windmill/commit/c0438479aa3b6dc6349df01abdd9dcc434fe8781))
* optimize performance for bun scripts without deps ([5b33f56](https://github.com/windmill-labs/windmill/commit/5b33f563e6e83605ae72338af351dcc97beb1a55))
* overflow on workspace script picker ([5e4db0e](https://github.com/windmill-labs/windmill/commit/5e4db0ebab616305928cfa455af6833335e0fcf9))
* tag id as flow ([#2318](https://github.com/windmill-labs/windmill/issues/2318)) ([f68cee4](https://github.com/windmill-labs/windmill/commit/f68cee4ebddbf6e774f80e91a8c89fb8dc213f91))
## [1.175.0](https://github.com/windmill-labs/windmill/compare/v1.174.0...v1.175.0) (2023-09-19)
@@ -37,7 +37,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -67,7 +67,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -28,7 +28,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO queue (id, script_hash, script_path, job_kind, language, tag, created_by, permissioned_as, email, scheduled_for, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
"query": "WITH uuid_table as (\n select gen_random_uuid() as uuid from generate_series(1, $11)\n )\n INSERT INTO queue \n (id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id)\n (SELECT uuid, $1, $2, $3, $4, ('{ \"uuid\": \"' || uuid || '\" }')::jsonb, $5, $6, $7, $8, $9, $10 FROM uuid_table) \n RETURNING id",
"describe": {
"columns": [
{
@@ -46,7 +46,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -70,5 +69,5 @@
false
]
},
"hash": "2de52e1f3226ca9281b6e25f74d4d05f4509cb87c875234bfc7b310a012e4d40"
"hash": "6b9ff3fbca9e825c95d14705082a10de88172c0c748a45aba4d2d03c3b58f54d"
}
@@ -60,7 +60,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",
+155 -152
View File
@@ -64,9 +64,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "1.0.5"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab"
dependencies = [
"memchr",
]
@@ -204,7 +204,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -334,7 +334,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -392,17 +392,6 @@ version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae"
[[package]]
name = "async-timer"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5fa6ed76cb2aa820707b4eb9ec46f42da9ce70b0eafab5e5e34942b38a44d5"
dependencies = [
"libc",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "async-trait"
version = "0.1.73"
@@ -411,7 +400,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -582,6 +571,19 @@ dependencies = [
"num-traits",
]
[[package]]
name = "bigdecimal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "454bca3db10617b88b566f205ed190aedb0e0e6dd4cad61d3988a72e8c5594cb"
dependencies = [
"autocfg",
"libm",
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "bincode"
version = "1.3.3"
@@ -610,7 +612,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.34",
"syn 2.0.37",
"which",
]
@@ -631,7 +633,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -889,7 +891,7 @@ version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72"
dependencies = [
"semver 1.0.18",
"semver 1.0.19",
"serde",
"toml 0.7.8",
"url",
@@ -979,9 +981,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.3"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
dependencies = [
"clap_builder",
"clap_derive",
@@ -989,9 +991,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.2"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
dependencies = [
"anstream",
"anstyle",
@@ -1008,7 +1010,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1315,7 +1317,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1337,7 +1339,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core 0.20.3",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1395,7 +1397,7 @@ checksum = "3c65c2ffdafc1564565200967edc4851c7b55422d3913466688907efd05ea26f"
dependencies = [
"deno-proc-macro-rules-macros",
"proc-macro2",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1407,7 +1409,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1527,7 +1529,7 @@ dependencies = [
"strum",
"strum_macros",
"syn 1.0.109",
"syn 2.0.34",
"syn 2.0.37",
"thiserror",
]
@@ -1748,9 +1750,9 @@ dependencies = [
[[package]]
name = "dyn-clone"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd"
[[package]]
name = "dyn-iter"
@@ -1937,7 +1939,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1965,7 +1967,7 @@ checksum = "b0fa992f1656e1707946bbba340ad244f0814009ef8c0118eb7b658395f19a2e"
dependencies = [
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1977,7 +1979,7 @@ dependencies = [
"frunk_core",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -1989,7 +1991,7 @@ dependencies = [
"frunk_core",
"frunk_proc_macro_helpers",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -2090,7 +2092,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -2363,9 +2365,9 @@ dependencies = [
[[package]]
name = "hermit-abi"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
@@ -2621,7 +2623,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -2631,7 +2633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix 0.38.13",
"rustix 0.38.14",
"windows-sys",
]
@@ -3054,9 +3056,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "md-5"
@@ -3071,10 +3073,11 @@ dependencies = [
[[package]]
name = "md-5"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest 0.10.7",
]
@@ -3149,7 +3152,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
"termcolor",
"thiserror",
]
@@ -3194,7 +3197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57349d5a326b437989b6ee4dc8f2f34b0cc131202748414712a8e7d98952fc8c"
dependencies = [
"base64 0.21.4",
"bigdecimal",
"bigdecimal 0.3.1",
"bindgen 0.68.1",
"bitflags 2.4.0",
"bitvec",
@@ -3416,7 +3419,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -3675,7 +3678,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -3713,7 +3716,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -3785,7 +3788,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -3842,7 +3845,7 @@ dependencies = [
"bytes",
"fallible-iterator",
"hmac",
"md-5 0.10.5",
"md-5 0.10.6",
"memchr",
"rand 0.8.5",
"sha2 0.10.7",
@@ -3894,7 +3897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
dependencies = [
"proc-macro2",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -4014,7 +4017,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"syn 2.0.34",
"syn 2.0.37",
"thiserror",
"typify",
"unicode-ident",
@@ -4034,7 +4037,7 @@ dependencies = [
"serde_json",
"serde_tokenstream",
"serde_yaml",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -4096,6 +4099,18 @@ dependencies = [
"serde",
]
[[package]]
name = "quick_cache"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f69f8d22fa3f34f3083d9a4375c038732c7a7e964de1beb81c544da92dfc40b8"
dependencies = [
"ahash 0.8.3",
"equivalent",
"hashbrown 0.14.0",
"parking_lot",
]
[[package]]
name = "quote"
version = "1.0.33"
@@ -4295,9 +4310,9 @@ dependencies = [
[[package]]
name = "rend"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab"
checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd"
dependencies = [
"bytecheck",
]
@@ -4348,18 +4363,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "retainer"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8c01a8276c14d0f8d51ebcf8a48f0748f9f73f5f6b29e688126e6a52bcb145"
dependencies = [
"async-lock",
"async-timer",
"log",
"rand 0.8.5",
]
[[package]]
name = "ring"
version = "0.16.20"
@@ -4481,7 +4484,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.34",
"syn 2.0.37",
"walkdir",
]
@@ -4539,7 +4542,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.18",
"semver 1.0.19",
]
[[package]]
@@ -4571,9 +4574,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.13"
version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662"
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno",
@@ -4590,7 +4593,7 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.5",
"rustls-webpki 0.101.6",
"sct",
]
@@ -4627,9 +4630,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.101.5"
version = "0.101.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed"
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
dependencies = [
"ring",
"untrusted",
@@ -4754,9 +4757,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.13"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
dependencies = [
"chrono",
"dyn-clone",
@@ -4768,9 +4771,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.13"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
dependencies = [
"proc-macro2",
"quote",
@@ -4849,9 +4852,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
dependencies = [
"serde",
]
@@ -4919,7 +4922,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -4995,7 +4998,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5052,9 +5055,9 @@ dependencies = [
[[package]]
name = "sha1"
version = "0.10.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -5170,9 +5173,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "smart-default"
@@ -5324,7 +5327,7 @@ checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53"
dependencies = [
"ahash 0.8.3",
"atoi",
"bigdecimal",
"bigdecimal 0.3.1",
"byteorder",
"bytes",
"chrono",
@@ -5409,7 +5412,7 @@ checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482"
dependencies = [
"atoi",
"base64 0.21.4",
"bigdecimal",
"bigdecimal 0.3.1",
"bitflags 2.4.0",
"byteorder",
"bytes",
@@ -5428,7 +5431,7 @@ dependencies = [
"hmac",
"itoa",
"log",
"md-5 0.10.5",
"md-5 0.10.6",
"memchr",
"once_cell",
"percent-encoding",
@@ -5454,7 +5457,7 @@ checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e"
dependencies = [
"atoi",
"base64 0.21.4",
"bigdecimal",
"bigdecimal 0.3.1",
"bitflags 2.4.0",
"byteorder",
"chrono",
@@ -5471,7 +5474,7 @@ dependencies = [
"home",
"itoa",
"log",
"md-5 0.10.5",
"md-5 0.10.6",
"memchr",
"num-bigint",
"once_cell",
@@ -5574,7 +5577,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5613,7 +5616,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5695,7 +5698,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5744,7 +5747,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5827,7 +5830,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5931,7 +5934,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5943,7 +5946,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5967,7 +5970,7 @@ dependencies = [
"proc-macro2",
"quote",
"swc_macros_common",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -5983,9 +5986,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.34"
version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ec6cdb6a4c16306eccf52ccd8d492e4ab64705a15a5016acb205251001bf72"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [
"proc-macro2",
"quote",
@@ -6013,7 +6016,7 @@ dependencies = [
"cfg-if",
"fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix 0.38.13",
"rustix 0.38.14",
"windows-sys",
]
@@ -6030,9 +6033,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
dependencies = [
"winapi-util",
]
@@ -6063,7 +6066,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -6089,9 +6092,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.28"
version = "0.3.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe"
dependencies = [
"deranged",
"itoa",
@@ -6102,15 +6105,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
@@ -6167,7 +6170,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -6267,9 +6270,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
@@ -6420,7 +6423,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
]
[[package]]
@@ -6558,7 +6561,7 @@ dependencies = [
"regress",
"schemars",
"serde_json",
"syn 2.0.34",
"syn 2.0.37",
"thiserror",
"unicode-ident",
]
@@ -6575,7 +6578,7 @@ dependencies = [
"serde",
"serde_json",
"serde_tokenstream",
"syn 2.0.34",
"syn 2.0.37",
"typify-impl",
]
@@ -6703,9 +6706,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "unicode-xid"
@@ -6893,7 +6896,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
"wasm-bindgen-shared",
]
@@ -6927,7 +6930,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.34",
"syn 2.0.37",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -7019,7 +7022,7 @@ version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888"
dependencies = [
"rustls-webpki 0.101.5",
"rustls-webpki 0.101.6",
]
[[package]]
@@ -7037,7 +7040,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.13",
"rustix 0.38.14",
]
[[package]]
@@ -7068,9 +7071,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
@@ -7083,7 +7086,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"axum",
@@ -7098,7 +7101,6 @@ dependencies = [
"prometheus",
"rand 0.8.5",
"reqwest",
"rsa 0.7.2",
"rsmq_async",
"serde",
"serde_json",
@@ -7118,7 +7120,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"argon2",
@@ -7138,16 +7140,17 @@ dependencies = [
"hex",
"hmac",
"hyper",
"itertools 0.10.5",
"itertools 0.11.0",
"lazy_static",
"magic-crypt",
"mail-send",
"mime_guess",
"prometheus",
"quick_cache",
"rand 0.8.5",
"regex",
"reqwest",
"retainer",
"rsa 0.7.2",
"rsmq_async",
"rust-embed",
"samael",
@@ -7177,7 +7180,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"base64 0.21.4",
"chrono",
@@ -7195,7 +7198,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"chrono",
"serde",
@@ -7208,7 +7211,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"axum",
@@ -7216,7 +7219,7 @@ dependencies = [
"hex",
"hmac",
"hyper",
"itertools 0.10.5",
"itertools 0.11.0",
"lazy_static",
"prometheus",
"rand 0.8.5",
@@ -7236,7 +7239,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"serde",
"serde_json",
@@ -7244,7 +7247,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -7255,11 +7258,11 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"gosyn",
"itertools 0.10.5",
"itertools 0.11.0",
"lazy_static",
"regex",
"windmill-parser",
@@ -7267,7 +7270,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -7278,10 +7281,10 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.11.0",
"rustpython-parser",
"serde_json",
"windmill-parser",
@@ -7289,11 +7292,11 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"async-recursion",
"itertools 0.10.5",
"itertools 0.11.0",
"lazy_static",
"phf 0.11.2",
"regex",
@@ -7306,7 +7309,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -7317,7 +7320,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -7334,7 +7337,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"getrandom 0.2.10",
@@ -7352,18 +7355,18 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"async-recursion",
"bigdecimal",
"bigdecimal 0.4.1",
"chrono",
"chrono-tz",
"cron",
"futures-core",
"hex",
"hmac",
"itertools 0.10.5",
"itertools 0.11.0",
"lazy_static",
"prometheus",
"reqwest",
@@ -7382,7 +7385,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.175.0"
version = "1.176.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -7401,7 +7404,7 @@ dependencies = [
"futures",
"gcp_auth",
"git-version",
"itertools 0.10.5",
"itertools 0.11.0",
"jsonwebtoken",
"lazy_static",
"mysql_async",
+3 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.175.0"
version = "1.176.0"
authors.workspace = true
edition.workspace = true
@@ -22,7 +22,7 @@ members = [
]
[workspace.package]
version = "1.175.0"
version = "1.176.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -54,7 +54,6 @@ tokio-metrics.workspace = true
rand.workspace = true
chrono.workspace = true
git-version.workspace = true
rsa.workspace = true
base64.workspace = true
sha2.workspace = true
rsmq_async.workspace = true
@@ -114,7 +113,7 @@ mime_guess = "^2"
hex = "^0"
sql-builder = "^3"
argon2 = "^0"
retainer = "^0"
quick_cache = "^0"
rand = "0.8.5"
rand_core = { version = "^0", features = ["std"] }
magic-crypt = "^3"
+43 -35
View File
@@ -1,41 +1,49 @@
#[cfg(feature = "enterprise")]
use base64::Engine;
#[cfg(feature = "enterprise")]
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
#[cfg(feature = "enterprise")]
use sha2::Sha256;
use windmill_common::error;
#[cfg(feature = "enterprise")]
pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
if let Some(license_key) = license_key {
let mut splitted_lk = license_key.split(".");
if splitted_lk.clone().count() != 3 {
panic!("license_key can be splitted with 2 . (<client id>.<expiry>.<signature>)");
}
let id = splitted_lk.next().unwrap();
let expiry = splitted_lk.next().unwrap();
let signature_b64 = splitted_lk.next().unwrap();
pub async fn set_license_key(license_key: String) -> anyhow::Result<()> {
use windmill_api::{ee::validate_license_key, LICENSE_KEY, LICENSE_KEY_ID, LICENSE_KEY_VALID};
let expiry_nb = expiry.parse::<u64>()?;
if expiry_nb < chrono::Utc::now().timestamp() as u64 {
panic!(
"License key is expired (timestamp expiry: {expiry_nb}. Now: {}",
chrono::Utc::now().timestamp()
);
}
const PUBLIC_KEY: &str = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB";
let pub_key = rsa::RsaPublicKey::from_public_key_der(
&base64::engine::general_purpose::STANDARD.decode(PUBLIC_KEY)?,
)?;
let signature = base64::engine::general_purpose::STANDARD.decode(signature_b64)?;
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
.verify(
&format!("{id}{expiry}").as_bytes(),
&rsa::pss::Signature::from(signature),
)
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;
} else {
panic!("License key is required for the enterprise edition");
let id = validate_license_key(license_key.clone()).await?;
{
let mut l = LICENSE_KEY_ID.write().await;
*l = id.to_string()
}
{
let mut l = LICENSE_KEY.write().await;
*l = license_key
}
{
let mut l = LICENSE_KEY_VALID.write().await;
*l = true
}
Ok(())
}
#[cfg(feature = "enterprise")]
pub async fn verify_license_key() -> error::Result<()> {
use windmill_api::{LICENSE_KEY, LICENSE_KEY_VALID};
use windmill_common::error::to_anyhow;
let expiry_nb = LICENSE_KEY
.read()
.await
.clone()
.split(".")
.nth(1)
.unwrap_or_else(|| "")
.parse::<u64>()
.map_err(to_anyhow)?;
if expiry_nb < chrono::Utc::now().timestamp() as u64 {
tracing::error!(
"License key expired: {} < {}",
expiry_nb,
chrono::Utc::now().timestamp() as u64
);
let mut l = LICENSE_KEY_VALID.write().await;
*l = false;
};
Ok(())
}
+49 -31
View File
@@ -19,15 +19,14 @@ use tokio::{
fs::{metadata, DirBuilder},
sync::RwLock,
};
use windmill_api::LICENSE_KEY;
use windmill_common::{
global_settings::{
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, OAUTH_SETTING,
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, LICENSE_KEY_SETTING, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING,
},
utils::rd_string,
worker::{reload_custom_tags_setting, WORKER_GROUP},
METRICS_ADDR,
DB, METRICS_ADDR,
};
use windmill_worker::{
BUN_CACHE_DIR, BUN_TMP_CACHE_DIR, DENO_CACHE_DIR, DENO_CACHE_DIR_DEPS, DENO_CACHE_DIR_NPM,
@@ -37,8 +36,8 @@ use windmill_worker::{
};
use crate::monitor::{
initial_load, monitor_db, reload_base_url_setting, reload_retention_period_setting,
reload_server_config, reload_worker_config,
initial_load, monitor_db, reload_base_url_setting, reload_license_key,
reload_retention_period_setting, reload_server_config, reload_worker_config,
};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
@@ -153,10 +152,10 @@ Windmill Community Edition {GIT_VERSION}
// since it's only on server mode, the port is statically defined
let base_internal_url: String = format!("http://localhost:{}", port.to_string());
monitor_db(&db, &base_internal_url, rsmq.clone(), server_mode).await;
initial_load(&db, tx.clone(), worker_mode, server_mode).await;
monitor_db(&db, &base_internal_url, rsmq.clone(), server_mode).await;
if std::env::var("BASE_INTERNAL_URL").is_ok() {
tracing::warn!("BASE_INTERNAL_URL is now unecessary and ignored, you can remove it.");
}
@@ -202,21 +201,7 @@ Windmill Community Edition {GIT_VERSION}
//monitor_db is applied at start, no need to apply it twice
tokio::time::sleep(Duration::from_secs(rd_delay)).await;
let mut listener = match PgListener::connect_with(&db).await {
Ok(l) => l,
Err(e) => {
tracing::error!(error = %e, "Could not connect to database");
return;
}
};
if let Err(e) = listener
.listen_all(vec!["notify_config_change", "notify_global_setting_change"])
.await
{
tracing::error!(error = %e, "Could not listen to database");
return;
}
let mut listener = retry_listen_pg(&db).await;
loop {
tokio::select! {
@@ -271,6 +256,12 @@ Windmill Community Edition {GIT_VERSION}
tracing::error!(error = %e, "Could not reload custom tags setting");
}
},
LICENSE_KEY_SETTING => {
tracing::info!("License Key setting change detected");
if let Err(e) = reload_license_key(&db).await {
tracing::error!(error = %e, "Could not reload license key setting");
}
},
RETENTION_PERIOD_SECS_SETTING => {
tracing::info!("Retention period setting change detected");
reload_retention_period_setting(&db).await
@@ -296,7 +287,8 @@ Windmill Community Edition {GIT_VERSION}
}
},
Err(e) => {
tracing::error!(error = %e, "Could not receive notification");
tracing::error!(error = %e, "Could not receive notification, attempting to reconnect listener");
listener = retry_listen_pg(&db).await;
continue;
}
};
@@ -330,6 +322,40 @@ Windmill Community Edition {GIT_VERSION}
Ok(())
}
async fn listen_pg(db: &DB) -> Option<PgListener> {
let mut listener = match PgListener::connect_with(&db).await {
Ok(l) => l,
Err(e) => {
tracing::error!(error = %e, "Could not connect to database");
return None;
}
};
if let Err(e) = listener
.listen_all(vec!["notify_config_change", "notify_global_setting_change"])
.await
{
tracing::error!(error = %e, "Could not listen to database");
return None;
}
return Some(listener);
}
async fn retry_listen_pg(db: &DB) -> PgListener {
let mut listener = listen_pg(db).await;
loop {
if listener.is_none() {
tracing::info!("Retrying listening to pg listen in 5 seconds");
tokio::time::sleep(Duration::from_secs(5)).await;
listener = listen_pg(db).await;
} else {
tracing::info!("Successfully connected to pg listen");
return listener.unwrap();
}
}
}
fn display_config(envs: &[&str]) {
tracing::info!(
"config: {}",
@@ -355,14 +381,6 @@ pub async fn run_workers<R: rsmq_async::RsmqConnection + Send + Sync + Clone + '
base_internal_url: String,
rsmq: Option<R>,
) -> anyhow::Result<()> {
#[cfg(feature = "enterprise")]
ee::verify_license_key(LICENSE_KEY.clone())?;
#[cfg(not(feature = "enterprise"))]
if LICENSE_KEY.as_ref().is_some_and(|x| !x.is_empty()) {
panic!("License key is required ONLY for the enterprise edition");
}
let instance_name = gethostname()
.to_str()
.map(|x| {
+73 -6
View File
@@ -15,10 +15,12 @@ use windmill_api::{
use windmill_common::{
error,
global_settings::{
BASE_URL_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING,
BASE_URL_SETTING, LICENSE_KEY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
RETENTION_PERIOD_SECS_SETTING,
},
jobs::{JobKind, QueuedJob},
server::load_server_config,
users::truncate_token,
worker::{load_worker_config, reload_custom_tags_setting, SERVER_CONFIG, WORKER_CONFIG},
BASE_URL, DB, METRICS_ENABLED,
};
@@ -26,6 +28,14 @@ use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SCRIPT_TOKEN_EXPIRY,
};
#[cfg(feature = "enterprise")]
use crate::ee::verify_license_key;
#[cfg(feature = "enterprise")]
use windmill_api::LICENSE_KEY_VALID;
use crate::ee::set_license_key;
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
@@ -79,11 +89,11 @@ pub async fn initial_load(
};
let reload_base_url_f = async {
if server_mode {
if let Err(e) = reload_base_url_setting(db).await {
tracing::error!("Error reloading custom tags: {:?}", e)
tracing::error!("Error reloading base url: {:?}", e)
}
}
};
let reload_server_config_f = async {
@@ -102,13 +112,24 @@ pub async fn initial_load(
reload_request_size(&db).await;
}
};
let reload_license_key_f = async {
if server_mode {
#[cfg(feature = "enterprise")]
if let Err(e) = reload_license_key(&db).await {
tracing::error!("Error reloading license key: {:?}", e)
}
}
};
join!(
reload_worker_config_f,
reload_server_config_f,
reload_custom_tags_f,
reload_request_size_f,
reload_base_url_f,
reload_retention_period_f
reload_retention_period_f,
reload_license_key_f
);
}
@@ -214,6 +235,36 @@ pub async fn reload_request_size(db: &DB) {
}
}
pub async fn reload_license_key(db: &DB) -> error::Result<()> {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
LICENSE_KEY_SETTING
)
.fetch_optional(db)
.await?;
let mut value = std::env::var("LICENSE_KEY")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or(String::new());
if let Some(q) = q {
if let Ok(v) = serde_json::from_value::<String>(q.value.clone()) {
tracing::info!(
"Loaded setting LICENSE_KEY from db config: {}",
truncate_token(&v)
);
value = v;
} else {
tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q.value);
}
};
set_license_key(value).await?;
Ok(())
}
pub async fn reload_setting<T: FromStr + DeserializeOwned + Display>(
db: &DB,
setting_name: &str,
@@ -271,12 +322,28 @@ pub async fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
}
};
let verify_license_key_f = async {
if server_mode {
#[cfg(feature = "enterprise")]
if let Err(e) = verify_license_key().await {
tracing::error!("Error verifying license key: {:?}", e);
let mut l = LICENSE_KEY_VALID.write().await;
*l = false;
}
}
};
let expose_queue_metrics_f = async {
if *METRICS_ENABLED && server_mode {
expose_queue_metrics(&db).await;
}
};
join!(expired_items_f, zombie_jobs_f, expose_queue_metrics_f);
join!(
expired_items_f,
zombie_jobs_f,
expose_queue_metrics_f,
verify_license_key_f
);
}
pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
+3 -3
View File
@@ -2,7 +2,6 @@ use std::sync::Arc;
#[cfg(feature = "enterprise")]
use chrono::Timelike;
#[cfg(feature = "enterprise")]
use futures::StreamExt;
use futures::{stream, Stream};
@@ -18,11 +17,12 @@ use windmill_api_client::types::{
CreateFlowBody, RawScript
};
#[cfg(feature = "enterprise")]
use sqlx::query;
#[cfg(feature = "enterprise")]
use windmill_api_client::types::{EditSchedule, NewSchedule, ScriptArgs, NewScript, NewScriptLanguage};
use windmill_api_client::types::{EditSchedule, NewSchedule, ScriptArgs};
use windmill_api_client::types::{NewScript, NewScriptLanguage};
use windmill_common::worker::WORKER_CONFIG;
use windmill_common::{
+2 -1
View File
@@ -53,7 +53,7 @@ cron.workspace = true
mime_guess.workspace = true
rust-embed.workspace = true
tracing-subscriber.workspace = true
retainer.workspace = true
quick_cache.workspace = true
rand.workspace = true
time.workspace = true
magic-crypt.workspace = true
@@ -74,3 +74,4 @@ bytes.workspace = true
mail-send.workspace = true
samael = { workspace = true, optional = true }
async-recursion.workspace = true
rsa.workspace = true
+28 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.175.0
version: 1.176.0
title: Windmill API
contact:
@@ -624,6 +624,32 @@ paths:
schema:
type: string
/settings/test_license_key:
post:
summary: test license key
operationId: testLicenseKey
tags:
- setting
requestBody:
description: test license key
required: true
content:
application/json:
schema:
type: object
properties:
license_key:
type: string
required:
- license_key
responses:
"200":
description: status
content:
text/plain::
schema:
type: string
/users/email:
get:
summary: get current user email (if logged in)
@@ -4117,6 +4143,7 @@ paths:
- $ref: "#/components/parameters/StartedBefore"
- $ref: "#/components/parameters/StartedAfter"
- $ref: "#/components/parameters/CreatedOrStartedBefore"
- $ref: "#/components/parameters/Running"
- $ref: "#/components/parameters/CreatedOrStartedAfter"
- $ref: "#/components/parameters/JobKinds"
- $ref: "#/components/parameters/ArgsFilter"
+41
View File
@@ -0,0 +1,41 @@
use anyhow::anyhow;
use base64::Engine;
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
use sha2::Sha256;
pub async fn validate_license_key(license_key: String) -> anyhow::Result<String> {
let mut splitted_lk = license_key.split(".");
if splitted_lk.clone().count() != 3 {
return Err(anyhow!(
"license_key can be splitted with 2 . (<client id>.<expiry>.<signature>)"
));
}
let id = splitted_lk.next().unwrap();
let expiry = splitted_lk.next().unwrap();
let signature_b64 = splitted_lk.next().unwrap();
const PUBLIC_KEY: &str = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB";
let pub_key = rsa::RsaPublicKey::from_public_key_der(
&base64::engine::general_purpose::STANDARD.decode(PUBLIC_KEY)?,
)?;
let signature = base64::engine::general_purpose::STANDARD.decode(signature_b64)?;
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
.verify(
&format!("{id}{expiry}").as_bytes(),
&rsa::pss::Signature::from(signature),
)
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;
let expiry_nb = expiry.parse::<u64>()?;
if expiry_nb < chrono::Utc::now().timestamp() as u64 {
tracing::error!(
"License key expired: {} < {}",
expiry_nb,
chrono::Utc::now().timestamp() as u64
);
return Err(anyhow!("License key expired".to_string()));
};
Ok(id.to_string())
}
+69 -8
View File
@@ -780,7 +780,14 @@ async fn list_jobs(
let (per_page, offset) = paginate(pagination);
let lqc = lq.clone();
let sqlc = list_completed_jobs_query(
if lq.success.is_some() && lq.running.is_some_and(|x| x) {
return Err(error::Error::BadRequest(
"cannot specify both success and running".to_string(),
));
}
let sqlc = if lq.running.is_none() {
Some(list_completed_jobs_query(
&w_id,
per_page + offset,
0,
@@ -817,7 +824,10 @@ async fn list_jobs(
"null as concurrent_limit",
"null as concurrency_time_window_s",
],
);
))
} else {
None
};
let sql = if lq.success.is_none() {
let sqlq = list_queue_jobs_query(
@@ -833,7 +843,7 @@ async fn list_jobs(
created_after: lq.created_after,
created_or_started_before: lq.created_or_started_before,
created_or_started_after: lq.created_or_started_after,
running: None,
running: lq.running,
parent_job: lq.parent_job,
order_desc: Some(true),
job_kinds: lq.job_kinds,
@@ -876,6 +886,7 @@ async fn list_jobs(
],
);
if let Some(sqlc) = sqlc {
format!(
"{} UNION ALL {} LIMIT {} OFFSET {};",
&sqlq.subquery()?,
@@ -883,8 +894,11 @@ async fn list_jobs(
per_page,
offset
)
} else {
sqlq.query()?
}
} else {
sqlc.query()?
sqlc.unwrap().query()?
};
let mut tx = user_db.begin(&authed).await?;
let jobs: Vec<UnifiedJob> = sqlx::query_as(&sql).fetch_all(&mut *tx).await?;
@@ -1636,6 +1650,19 @@ async fn check_tag_available_for_workspace(w_id: &str, tag: &Option<String>) ->
}
}
#[cfg(feature = "enterprise")]
pub async fn check_license_key_valid() -> error::Result<()> {
use crate::LICENSE_KEY_VALID;
let valid = *LICENSE_KEY_VALID.read().await;
if !valid {
return Err(error::Error::BadRequest(format!(
"License key is not valid. Go to your superadmin settings to update your license key.",
)));
}
Ok(())
}
pub async fn run_flow_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -1646,6 +1673,8 @@ pub async fn run_flow_by_path(
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("run:flow/{flow_path}"))?;
@@ -1699,7 +1728,11 @@ pub async fn run_job_by_path(
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let script_path = script_path.to_path();
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let (job_payload, tag) = script_path_to_payload(script_path, &db, &w_id).await?;
@@ -1887,6 +1920,9 @@ pub async fn run_wait_result_job_by_path_get(
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
if method == http::Method::HEAD {
return Ok(Json(serde_json::json!("")));
}
@@ -1947,6 +1983,9 @@ pub async fn run_wait_result_flow_by_path_get(
headers: HeaderMap,
Query(run_query): Query<RunJobQuery>,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
if method == http::Method::HEAD {
return Ok(Json(serde_json::json!("")));
}
@@ -1986,6 +2025,9 @@ pub async fn run_wait_result_script_by_path(
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
run_wait_result_script_by_path_internal(
db,
run_query,
@@ -2111,6 +2153,9 @@ pub async fn run_wait_result_script_by_hash(
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
check_queue_too_long(&db, run_query.queue_limit).await?;
let hash = script_hash.0;
@@ -2201,6 +2246,9 @@ pub async fn run_wait_result_flow_by_path(
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::JsonResult<serde_json::Value> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
run_wait_result_flow_by_path_internal(
db, run_query, flow_path, authed, rsmq, user_db, headers, args, raw_string, w_id,
)
@@ -2276,6 +2324,9 @@ async fn run_preview_job(
headers: HeaderMap,
Json(preview): Json<Preview>,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
check_scopes(&authed, || format!("runscript"))?;
if authed.is_operator {
return Err(error::Error::NotAuthorized(
@@ -2332,7 +2383,6 @@ struct BatchInfo {
kind: String,
flow_value: Option<FlowValue>,
path: Option<String>,
dedicated_worker: Option<bool>,
}
#[tracing::instrument(level = "trace", skip_all)]
@@ -2362,7 +2412,7 @@ async fn add_batch_jobs(
batch_info.path,
JobKind::Script,
Some(script.language),
batch_info.dedicated_worker,
script.dedicated_worker,
)
}
"flow" => {
@@ -2433,15 +2483,22 @@ async fn add_batch_jobs(
format!("{}", language.as_str())
};
let uuids = sqlx::query_scalar!("INSERT INTO queue (id, script_hash, script_path, job_kind, language, tag, created_by, permissioned_as, email, scheduled_for, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11)) RETURNING id",
let uuids = sqlx::query_scalar!(
r#"WITH uuid_table as (
select gen_random_uuid() as uuid from generate_series(1, $11)
)
INSERT INTO queue
(id, script_hash, script_path, job_kind, language, args, tag, created_by, permissioned_as, email, scheduled_for, workspace_id)
(SELECT uuid, $1, $2, $3, $4, ('{ "uuid": "' || uuid || '" }')::jsonb, $5, $6, $7, $8, $9, $10 FROM uuid_table)
RETURNING id"#,
hash.map(|h| h.0),
path,
job_kind.clone() as JobKind,
language as ScriptLang,
tag,
authed.username,
authed.email,
username_to_permissioned_as(&authed.username),
authed.email,
Utc::now(),
w_id,
n
@@ -2512,6 +2569,9 @@ pub async fn run_job_by_hash(
headers: HeaderMap,
JsonOrForm(args, raw_string): JsonOrForm,
) -> error::Result<(StatusCode, String)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let hash = script_hash.0;
let (
path,
@@ -2715,6 +2775,7 @@ pub struct ListCompletedQuery {
pub created_or_started_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_or_started_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>,
pub running: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
pub job_kinds: Option<String>,
+5 -10
View File
@@ -47,6 +47,7 @@ mod capture;
mod configs;
mod db;
mod drafts;
pub mod ee;
mod favorite;
mod flows;
mod folders;
@@ -101,7 +102,9 @@ lazy_static::lazy_static! {
slack: None
}));
pub static ref LICENSE_KEY: Option<String> = std::env::var("LICENSE_KEY").ok();
pub static ref LICENSE_KEY_VALID: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
pub static ref LICENSE_KEY_ID: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
pub static ref LICENSE_KEY: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
}
pub async fn run_server(
@@ -268,8 +271,6 @@ pub async fn run_server(
println!("Graceful shutdown of server");
});
tokio::spawn(async move { auth_cache.monitor().await });
server.await?;
Ok(())
}
@@ -317,13 +318,7 @@ async fn ee_license() -> &'static str {
#[cfg(feature = "enterprise")]
async fn ee_license() -> String {
LICENSE_KEY
.as_ref()
.unwrap()
.split(".")
.next()
.unwrap()
.to_string()
LICENSE_KEY_ID.read().await.clone()
}
async fn openapi() -> &'static str {
+81 -50
View File
@@ -182,7 +182,7 @@ pub fn build_oauth_clients(
.as_ref()
.map(|c| (x.0.clone(), (x.1, c.clone())))
}))
.map(|(k, (client_params, config))| {
.filter_map(|(k, (client_params, config))| {
let named_client = build_basic_client(
k.clone(),
config.clone(),
@@ -191,17 +191,25 @@ pub fn build_oauth_clients(
base_url,
None,
);
(
named_client.0,
ClientWithScopes {
client: named_client.1,
scopes: config.scopes.unwrap_or(vec![]),
extra_params: config.extra_params,
extra_params_callback: config.extra_params_callback,
allowed_domains: client_params.allowed_domains.clone(),
userinfo_url: config.userinfo_url,
},
)
named_client
.map(|named_client| {
(
named_client.0,
ClientWithScopes {
client: named_client.1,
scopes: config.scopes.unwrap_or(vec![]),
extra_params: config.extra_params,
extra_params_callback: config.extra_params_callback,
allowed_domains: client_params.allowed_domains.clone(),
userinfo_url: config.userinfo_url,
},
)
})
.map_err(|e| {
tracing::error!("Error building oauth client {k}: {e}");
e
})
.ok()
})
.collect();
@@ -213,7 +221,7 @@ pub fn build_oauth_clients(
.as_ref()
.map(|c| (x.0.clone(), (x.1, c.clone())))
}))
.map(|(k, (client_params, config))| {
.filter_map(|(k, (client_params, config))| {
let named_client = build_basic_client(
k.clone(),
config.clone(),
@@ -226,43 +234,61 @@ pub fn build_oauth_clients(
None
},
);
(
named_client.0,
ClientWithScopes {
client: named_client.1,
scopes: config.scopes.unwrap_or(vec![]),
extra_params: config.extra_params,
extra_params_callback: config.extra_params_callback,
allowed_domains: None,
userinfo_url: None,
},
)
named_client
.map(|named_client| {
(
named_client.0,
ClientWithScopes {
client: named_client.1,
scopes: config.scopes.unwrap_or(vec![]),
extra_params: config.extra_params,
extra_params_callback: config.extra_params_callback,
allowed_domains: None,
userinfo_url: None,
},
)
})
.map_err(|e| {
tracing::error!("Error building oauth client {k}: {e}");
e
})
.ok()
})
.collect();
let slack = oauths.get("slack").map(|v| {
build_basic_client(
"slack".to_string(),
OAuthConfig {
auth_url: "https://slack.com/oauth/authorize".to_string(),
token_url: "https://slack.com/api/oauth.access".to_string(),
userinfo_url: None,
scopes: None,
extra_params: None,
extra_params_callback: None,
req_body_auth: None,
},
v.clone(),
false,
base_url,
Some(format!("{base_url}/oauth/callback_slack")),
)
.1
});
Ok(AllClients { logins, connects, slack })
let slack = oauths
.get("slack")
.map(|v| {
build_basic_client(
"slack".to_string(),
OAuthConfig {
auth_url: "https://slack.com/oauth/authorize".to_string(),
token_url: "https://slack.com/api/oauth.access".to_string(),
userinfo_url: None,
scopes: None,
extra_params: None,
extra_params_callback: None,
req_body_auth: None,
},
v.clone(),
false,
base_url,
Some(format!("{base_url}/oauth/callback_slack")),
)
.map(|x| x.1)
.map_err(|e| {
tracing::error!("Error building oauth slack client: {e}");
e
})
.ok()
})
.flatten();
let all_clients = AllClients { logins, connects, slack };
tracing::info!("Final oauth config: {all_clients:#?}");
Ok(all_clients)
}
use anyhow::anyhow;
pub fn build_basic_client(
name: String,
config: OAuthConfig,
@@ -270,9 +296,11 @@ pub fn build_basic_client(
login: bool,
base_url: &str,
override_callback: Option<String>,
) -> (String, OClient) {
let auth_url = Url::parse(&config.auth_url).expect("Invalid authorization endpoint URL");
let token_url = Url::parse(&config.token_url).expect("Invalid token endpoint URL");
) -> error::Result<(String, OClient)> {
let auth_url = Url::parse(&config.auth_url)
.map_err(|e| anyhow!("Invalid authorization endpoint URL: {e}"))?;
let token_url =
Url::parse(&config.token_url).map_err(|e| anyhow!("Invalid token endpoint URL: {e}"))?;
let redirect_url = if login {
format!("{base_url}/user/login_callback/{name}")
@@ -287,9 +315,12 @@ pub fn build_basic_client(
client.set_auth_type(AuthType::RequestBody);
}
client.set_client_secret(client_params.secret.clone());
client.set_redirect_url(Url::parse(&redirect_url).expect("Invalid redirect URL"));
client.set_redirect_url(
Url::parse(&redirect_url).map_err(|e| anyhow!("Invalid redirect URL: {e}"))?,
);
// Set up the config for the Github OAuth2 process.
(name.to_string(), client)
Ok((name.to_string(), client))
}
#[derive(Clone, Debug, Deserialize, Serialize)]
+18
View File
@@ -10,6 +10,7 @@ use std::time::Duration;
use crate::{
db::{ApiAuthed, DB},
ee::validate_license_key,
utils::require_super_admin,
};
@@ -36,6 +37,7 @@ pub fn global_service() -> Router {
post(set_global_setting).get(get_global_setting),
)
.route("/test_smtp", post(test_email))
.route("/test_license_key", post(test_license_key))
}
#[derive(Deserialize)]
@@ -72,6 +74,22 @@ pub async fn test_email(
Ok("Sent test email".to_string())
}
#[derive(Deserialize)]
pub struct TestKey {
pub license_key: String,
}
pub async fn test_license_key(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(TestKey { license_key }): Json<TestKey>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
validate_license_key(license_key).await?;
Ok("Sent test email".to_string())
}
pub async fn get_local_settings(
Extension(db): Extension<DB>,
authed: ApiAuthed,
+26 -26
View File
@@ -8,7 +8,7 @@
#![allow(non_snake_case)]
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use crate::db::ApiAuthed;
@@ -33,15 +33,16 @@ use hyper::{header::LOCATION, StatusCode};
use lazy_static::lazy_static;
use mail_send::mail_builder::MessageBuilder;
use mail_send::SmtpClientBuilder;
use quick_cache::sync::Cache;
use rand::rngs::OsRng;
use regex::Regex;
use retainer::Cache;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use time::OffsetDateTime;
use tower_cookies::{Cookie, Cookies};
use tracing::{Instrument, Span};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::users::truncate_token;
use windmill_common::worker::{CLOUD_HOSTED, SERVER_CONFIG};
use windmill_common::{
db::UserDB,
@@ -50,7 +51,6 @@ use windmill_common::{
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
const TTL_TOKEN_CACHE_S: u64 = 60; // 60s
pub const TTL_TOKEN_DB_H: u32 = 72;
const COOKIE_NAME: &str = "token";
@@ -103,19 +103,24 @@ pub fn make_unauthed_service() -> Router {
.route("/logout", get(logout))
}
#[derive(Clone)]
pub struct ExpiringAuthCache {
pub authed: ApiAuthed,
pub expiry: Option<chrono::DateTime<chrono::Utc>>,
}
pub struct AuthCache {
cache: Cache<(String, String), ApiAuthed>,
cache: Cache<(String, String), ExpiringAuthCache>,
db: DB,
superadmin_secret: Option<String>,
}
impl AuthCache {
pub fn new(db: DB, superadmin_secret: Option<String>) -> Self {
AuthCache { cache: Cache::new(), db, superadmin_secret }
AuthCache { cache: Cache::new(300), db, superadmin_secret }
}
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token)).await;
self.cache.remove(&(w_id.to_string(), token));
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<ApiAuthed> {
@@ -123,13 +128,17 @@ impl AuthCache {
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
token.to_string(),
);
let s = self.cache.get(&key).await.map(|c| c.to_owned());
let s = self.cache.get(&key).map(|c| c.to_owned());
match s {
a @ Some(_) => a,
None => {
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>)>(
Some(ExpiringAuthCache { authed, expiry })
if expiry.is_none() || expiry.unwrap() > chrono::Utc::now() =>
{
Some(authed)
}
_ => {
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>, Option<chrono::DateTime<chrono::Utc>>)>(
"UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \
OR expiration IS NULL) RETURNING owner, email, super_admin, scopes",
OR expiration IS NULL) RETURNING owner, email, super_admin, scopes, expiration",
)
.bind(token)
.fetch_optional(&self.db)
@@ -140,7 +149,7 @@ impl AuthCache {
if let Some(user) = user_o {
let authed_o = {
match user {
(Some(owner), Some(email), super_admin, _) if w_id.is_some() => {
(Some(owner), Some(email), super_admin, _, _) if w_id.is_some() => {
if let Some((prefix, name)) = owner.split_once('/') {
if prefix == "u" {
let (is_admin, is_operator) = if super_admin {
@@ -219,7 +228,7 @@ impl AuthCache {
})
}
}
(_, Some(email), super_admin, scopes) => {
(_, Some(email), super_admin, scopes, _) => {
if w_id.is_some() {
let row_o = sqlx::query_as::<_, (String, bool, bool)>(
"SELECT username, is_admin, operator FROM usr where email = $1 AND \
@@ -289,9 +298,10 @@ impl AuthCache {
}
};
if let Some(authed) = authed_o.as_ref() {
self.cache
.insert(key, authed.clone(), Duration::from_secs(TTL_TOKEN_CACHE_S))
.await;
self.cache.insert(
key,
ExpiringAuthCache { authed: authed.clone(), expiry: user.4 },
);
}
authed_o
} else if self
@@ -315,10 +325,6 @@ impl AuthCache {
}
}
}
pub async fn monitor(&self) {
self.cache.monitor(20, 0.25, Duration::from_secs(10)).await;
}
}
async fn extract_token<S: Send + Sync>(parts: &mut Parts, state: &S) -> Option<String> {
@@ -2359,9 +2365,3 @@ pub async fn login_externally(
tx.commit().await?;
Ok(())
}
pub fn truncate_token(token: &str) -> String {
let mut s = token[..10].to_owned();
s.push_str("*****");
s
}
+7 -13
View File
@@ -1,7 +1,8 @@
use std::time::Duration;
use quick_cache::sync::Cache;
use serde::Serialize;
use tokio::{select, sync::mpsc, time::interval};
use tokio::{select, sync::mpsc};
use windmill_common::METRICS_ENABLED;
use crate::db::DB;
@@ -78,8 +79,7 @@ impl WebhookShared {
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let cache = retainer::Cache::new();
let mut cache_purge_interval = interval(Duration::from_secs(30));
let cache = Cache::new(100);
loop {
select! {
@@ -87,7 +87,7 @@ impl WebhookShared {
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
let url_guard = match cache.get(&workspace_id).await {
let webhook_opt = match cache.get(&workspace_id) {
Some(guard) => {
guard
},
@@ -104,16 +104,14 @@ impl WebhookShared {
tracing::error!("Webhook Message to send - but cannot get workspace settings! Workspace: {workspace_id}");
continue;
};
cache.insert(workspace_id.clone(), webook_opt, Duration::from_secs(30)).await;
cache.get(&workspace_id).await.unwrap()
cache.insert(workspace_id, webook_opt.clone());
webook_opt
}
};
let webook_opt = url_guard.value();
if let Some(url) = webook_opt {
if let Some(url) = webhook_opt {
let timer = if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let _ = client.post(url).json(&message).send().await;
timer.map(|x| x.stop_and_record());
drop(url_guard);
}
},
Some(WebhookPayload::InstanceEvent(event)) => {
@@ -125,10 +123,6 @@ impl WebhookShared {
},
None => break,
},
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
tracing::trace!("Purging Webhook Cache");
cache.purge(10, 0.50).await;
},
}
}
});
@@ -4,6 +4,7 @@ pub const BASE_URL_SETTING: &str = "base_url";
pub const OAUTH_SETTING: &str = "oauths";
pub const RETENTION_PERIOD_SECS_SETTING: &str = "retention_period_secs";
pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb";
pub const LICENSE_KEY_SETTING: &str = "license_key";
pub const ENV_SETTINGS: [&str; 54] = [
"DISABLE_NSJAIL",
+1
View File
@@ -161,6 +161,7 @@ pub struct Script {
pub concurrent_limit: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_time_window_s: Option<i32>,
pub dedicated_worker: Option<bool>,
}
#[derive(Serialize)]
+6
View File
@@ -15,3 +15,9 @@ pub fn username_to_permissioned_as(user: &str) -> String {
format!("u/{}", user)
}
}
pub fn truncate_token(token: &str) -> String {
let mut s = token[..10].to_owned();
s.push_str("*****");
s
}
+6 -9
View File
@@ -78,15 +78,12 @@ pub async fn reload_custom_tags_setting(db: &DB) -> error::Result<()> {
let custom_tags = process_custom_tags(tags);
{
let l = CUSTOM_TAGS_PER_WORKSPACE.read().await;
if l.clone() == custom_tags {
tracing::info!("Custom tags setting unchanged, skipping update");
return Ok(());
} else {
tracing::info!("Custom tags setting changed, updating");
}
}
tracing::info!(
"Loaded setting custom tags, common: {:?}, per-workspace: {:?}",
custom_tags.0,
custom_tags.1,
);
{
let mut l = CUSTOM_TAGS_PER_WORKSPACE.write().await;
*l = custom_tags.clone()
+4 -4
View File
@@ -1744,11 +1744,11 @@ pub async fn push<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
tag = None;
}
let default = || {
if job_kind == JobKind::Flow || job_kind == JobKind::FlowPreview {
if job_kind == JobKind::Flow
|| job_kind == JobKind::FlowPreview
|| job_kind == JobKind::Identity
{
"flow".to_string()
} else if job_kind == JobKind::Identity {
// identity is a light script, nativets is too
"nativets".to_string()
} else if job_kind == JobKind::Dependencies || job_kind == JobKind::FlowDependencies {
"dependency".to_string()
} else {
+33 -13
View File
@@ -219,18 +219,19 @@ pub async fn handle_bun_job(
})?,
)
.await?;
install_lockfile(
logs,
&job.id,
&job.workspace_id,
db,
job_dir,
worker_name,
common_bun_proc_envs.clone(),
)
.await?;
remove_dir_all(format!("{}/node_modules", job_dir)).await?;
}
install_lockfile(
logs,
&job.id,
&job.workspace_id,
db,
job_dir,
worker_name,
common_bun_proc_envs.clone(),
)
.await?;
remove_dir_all(format!("{}/node_modules", job_dir)).await?;
} else if !*DISABLE_NSJAIL {
logs.push_str("\n\n--- BUN INSTALL ---\n");
set_logs(&logs, &job.id, &db).await;
@@ -464,6 +465,10 @@ pub async fn start_worker(
mut jobs_rx: Receiver<QueuedJob>,
mut killpill_rx: tokio::sync::broadcast::Receiver<()>,
) -> Result<()> {
use std::task::Poll;
use futures::{future, Future};
let mut logs = "".to_string();
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
@@ -684,6 +689,21 @@ plugin(p)
// let mut i = 0;
// let mut j = 0;
let mut alive = true;
fn conditional_polling<T>(
fut: impl Future<Output = T>,
predicate: bool,
) -> impl Future<Output = T> {
let mut fut = Box::pin(fut);
future::poll_fn(move |cx| {
if predicate {
fut.as_mut().poll(cx)
} else {
Poll::Pending
}
})
}
loop {
tokio::select! {
biased;
@@ -711,8 +731,8 @@ plugin(p)
tracing::info!("dedicated worker process exited");
break;
}
}
job = jobs_rx.recv(), if alive && jobs.len() < MAX_BUFFERED_DEDICATED_JOBS => {
},
job = conditional_polling(jobs_rx.recv(), alive && jobs.len() < MAX_BUFFERED_DEDICATED_JOBS) => {
// i += 1;
if let Some(job) = job {
tracing::debug!("received job");
+229 -33
View File
@@ -10,12 +10,16 @@ use anyhow::Result;
use const_format::concatcp;
use itertools::Itertools;
use once_cell::sync::OnceCell;
use prometheus::core::{AtomicU64, GenericCounter};
#[cfg(feature = "benchmark")]
use serde::Serialize;
use sqlx::{Pool, Postgres};
use std::{
collections::HashMap,
sync::{atomic::Ordering, Arc},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
time::Duration,
};
use windmill_api_client::Client;
@@ -337,6 +341,52 @@ macro_rules! add_time {
};
}
async fn handle_receive_completed_job<
R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static,
>(
jc: JobCompleted,
worker_execution_failed: HashMap<Option<ScriptLang>, GenericCounter<AtomicU64>>,
base_internal_url: String,
db: Pool<Postgres>,
worker_dir: String,
same_worker_tx: Sender<Uuid>,
rsmq: Option<R>,
) {
let metrics = build_language_metrics(&worker_execution_failed.clone(), &jc.job.language);
let token = jc.token.clone();
let workspace = jc.job.workspace_id.clone();
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
workspace,
token,
client: OnceCell::new(),
};
if let Err(err) = process_completed_job(
&jc,
&client,
&db,
&worker_dir,
metrics.clone(),
same_worker_tx.clone(),
rsmq.clone(),
)
.await
{
handle_job_error(
&db,
&client,
&jc.job,
err,
metrics,
false,
same_worker_tx.clone(),
&worker_dir,
rsmq.clone(),
)
.await;
}
}
pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>(
db: &Pool<Postgres>,
worker_instance: &str,
@@ -512,8 +562,6 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
#[cfg(feature = "enterprise")]
let mut copy_cache_from_bucket_handle: Option<tokio::task::JoinHandle<()>> = None;
tracing::info!(worker = %worker_name, "starting worker");
#[cfg(feature = "enterprise")]
let mut last_sync = Instant::now()
+ Duration::from_secs(rand::thread_rng().gen_range(0..*GLOBAL_CACHE_INTERVAL));
@@ -549,43 +597,168 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let rsmq2 = rsmq.clone();
let worker_dir2 = worker_dir.clone();
let worker_execution_failed2 = worker_execution_failed.clone();
let thread_count = Arc::new(AtomicUsize::new(0));
let is_dedicated_worker = WORKER_CONFIG.read().await.dedicated_worker.is_some();
#[cfg(feature = "benchmark")]
let jobs = 25000;
#[cfg(feature = "benchmark")]
{
if is_dedicated_worker {
// you need to create the script first, check https://github.com/windmill-labs/windmill/blob/b76a92cfe454c686f005c65f534e29e039f3c706/benchmarks/lib.ts#L47
let hash = sqlx::query_scalar!(
"SELECT hash FROM script WHERE path = $1 AND workspace_id = $2",
"f/benchmarks/dedicated",
"admins"
)
.fetch_one(db)
.await
.unwrap_or_else(|_e| panic!("failed to insert dedicated jobs"));
sqlx::query!("INSERT INTO queue (id, script_hash, script_path, job_kind, language, tag, created_by, permissioned_as, email, scheduled_for, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11))",
hash,
"f/benchmarks/dedicated",
JobKind::Script as JobKind,
ScriptLang::Bun as ScriptLang,
"admins:f/benchmarks/dedicated",
"admin",
"u/admin",
"admin@windmill.dev",
chrono::Utc::now(),
"admins",
jobs
)
.execute(db)
.await.unwrap_or_else(|_e| panic!("failed to insert dedicated jobs"));
} else {
sqlx::query!("INSERT INTO queue (id, script_hash, script_path, job_kind, language, tag, created_by, permissioned_as, email, scheduled_for, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, $11))",
None::<i64>,
None::<String>,
JobKind::Noop as JobKind,
ScriptLang::Deno as ScriptLang,
"deno",
"admin",
"u/admin",
"admin@windmill.dev",
chrono::Utc::now(),
"admins",
jobs
)
.execute(db)
.await.unwrap_or_else(|_e| panic!("failed to insert noop jobs"));
}
}
#[cfg(feature = "benchmark")]
let completed_jobs = Arc::new(AtomicUsize::new(0));
#[cfg(feature = "benchmark")]
let start = Instant::now();
#[cfg(feature = "benchmark")]
let main_duration = Arc::new(AtomicUsize::new(0));
#[cfg(feature = "benchmark")]
let send_duration = Arc::new(AtomicUsize::new(0));
#[cfg(feature = "benchmark")]
let process_duration = Arc::new(AtomicUsize::new(0));
#[cfg(feature = "benchmark")]
let main_duration2 = main_duration.clone();
#[cfg(feature = "benchmark")]
let send_duration2 = send_duration.clone();
let send_result = tokio::spawn(async move {
while let Some(jc) = job_completed_rx.recv().await {
let metrics = build_language_metrics(&worker_execution_failed2, &jc.job.language);
let token = jc.token.clone();
let workspace = jc.job.workspace_id.clone();
let client = AuthedClient {
base_internal_url: base_internal_url2.to_string(),
workspace,
token,
client: OnceCell::new(),
};
if let Err(err) = process_completed_job(
&jc,
&client,
&db2,
&worker_dir2,
metrics.clone(),
same_worker_tx2.clone(),
rsmq2.clone(),
)
.await
{
handle_job_error(
&db2,
&client,
&jc.job,
err,
metrics,
false,
same_worker_tx2.clone(),
&worker_dir2,
rsmq2.clone(),
let base_internal_url2 = base_internal_url2.clone();
let worker_execution_failed2 = worker_execution_failed2.clone();
let worker_dir2 = worker_dir2.clone();
let db2 = db2.clone();
let same_worker_tx2 = same_worker_tx2.clone();
let rsmq2 = rsmq2.clone();
if matches!(jc.job.job_kind, JobKind::Noop) || is_dedicated_worker {
thread_count.fetch_add(1, Ordering::SeqCst);
let thread_count = thread_count.clone();
#[cfg(feature = "benchmark")]
let send_duration = send_duration2.clone();
#[cfg(feature = "benchmark")]
let process_duration = process_duration.clone();
#[cfg(feature = "benchmark")]
let completed_jobs = completed_jobs.clone();
#[cfg(feature = "benchmark")]
let main_duration = main_duration2.clone();
tokio::spawn(async move {
#[cfg(feature = "benchmark")]
let process_start = Instant::now();
handle_receive_completed_job(
jc,
worker_execution_failed2,
base_internal_url2,
db2,
worker_dir2,
same_worker_tx2,
rsmq2,
)
.await;
#[cfg(feature = "benchmark")]
{
let n = completed_jobs.fetch_add(1, Ordering::SeqCst);
if (n + 1) % 1000 == 0 || n == (jobs - 1) as usize {
let duration_s = start.elapsed().as_secs_f64();
let jobs_per_sec = n as f64 / duration_s;
tracing::info!(
"completed {} jobs in {}s, {} jobs/s",
n + 1,
duration_s,
jobs_per_sec
);
tracing::info!(
"main loop without send {}s",
main_duration.load(Ordering::SeqCst) as f64 / 1000.0
);
tracing::info!(
"send job completed / send dedicated job duration {}s",
send_duration.load(Ordering::SeqCst) as f64 / 1000.0
);
tracing::info!(
"job completed process duration {}s",
process_duration.load(Ordering::SeqCst) as f64 / 1000.0
);
}
process_duration.fetch_add(
process_start.elapsed().as_millis() as usize,
Ordering::SeqCst,
);
}
thread_count.fetch_sub(1, Ordering::SeqCst);
});
} else {
handle_receive_completed_job(
jc,
worker_execution_failed2,
base_internal_url2,
db2,
worker_dir2,
same_worker_tx2,
rsmq2,
)
.await;
}
}
tracing::info!("stopped processing new completed jobs");
while thread_count.load(Ordering::SeqCst) > 0 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
tracing::info!("finished processing all completed jobs");
// if let Err(e) =
// add_completed_job(&db2, &job, success, false, result, logs, rsmq2.clone()).await
// {
@@ -719,6 +892,9 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
(None, None) as (Option<Sender<QueuedJob>>, Option<JoinHandle<()>>)
};
#[cfg(feature = "benchmark")]
tracing::info!("pre loop time {}s", start.elapsed().as_secs_f64());
loop {
#[cfg(feature = "benchmark")]
let loop_start = Instant::now();
@@ -889,11 +1065,27 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
jobs_executed += 1;
if let Some(dedicated_worker_tx) = dedicated_worker_tx.clone() {
#[cfg(feature = "benchmark")]
main_duration
.fetch_add(loop_start.elapsed().as_millis() as usize, Ordering::SeqCst);
#[cfg(feature = "benchmark")]
let send_start = Instant::now();
if let Err(e) = dedicated_worker_tx.send(job.clone()).await {
tracing::info!("failed to send jobs to dedicated workers. Likely dedicated worker has been shut down. This is normal: {e:?}");
}
#[cfg(feature = "benchmark")]
send_duration
.fetch_add(send_start.elapsed().as_millis() as usize, Ordering::SeqCst);
continue;
} else if matches!(job.job_kind, JobKind::Noop) {
#[cfg(feature = "benchmark")]
main_duration
.fetch_add(loop_start.elapsed().as_millis() as usize, Ordering::SeqCst);
#[cfg(feature = "benchmark")]
let send_start = Instant::now();
job_completed_tx
.send(JobCompleted {
job,
@@ -905,6 +1097,10 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
})
.await
.expect("send job completed");
#[cfg(feature = "benchmark")]
send_duration
.fetch_add(send_start.elapsed().as_millis() as usize, Ordering::SeqCst);
} else {
let token = create_token_for_owner_in_bg(&db, &job).await;
+112 -56
View File
@@ -11,6 +11,31 @@ import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import { VERSION, createBenchScript, getFlowPayload, login } from "./lib.ts";
async function verifyOutputs(uuids: string[], workspace: string) {
console.log("Verifying outputs");
let incorrectResults = 0;
for (const uuid of uuids) {
try {
const job = await windmill.JobService.getCompletedJob({
workspace,
id: uuid,
});
if (!job.success) {
console.log(`Job ${uuid} did not complete`);
incorrectResults++;
}
if (job.result !== uuid) {
console.log(`Job ${uuid} did not output the correct value`);
incorrectResults++;
}
} catch (_) {
console.log(`Job ${uuid} did not complete`);
incorrectResults++;
}
}
console.log(`Incorrect results: ${incorrectResults}`);
}
export async function main({
host,
email,
@@ -19,6 +44,7 @@ export async function main({
workspace,
kind,
jobs,
noVerify,
}: {
host: string;
email?: string;
@@ -27,6 +53,7 @@ export async function main({
workspace: string;
kind: string;
jobs: number;
noVerify?: boolean;
}) {
windmill.setClient("", host);
@@ -37,6 +64,9 @@ export async function main({
host,
email,
workspace,
kind,
jobs,
noVerify,
},
null,
4
@@ -65,11 +95,37 @@ export async function main({
windmill.setClient(final_token, host);
const enc = (s: string) => new TextEncoder().encode(s);
async function getQueueCount() {
return (
await (
await fetch(
config.server + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } }
)
).json()
).database_length;
}
let pastJobs = 0;
async function getCompletedJobsCount(): Promise<number> {
const completedJobs = (
await (
await fetch(
host + "/api/w/" + config.workspace_id + "/jobs/completed/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } }
)
).json()
).database_length;
return completedJobs - pastJobs;
}
if (["deno", "python", "go", "bash", "dedicated", "bun"].includes(kind)) {
await createBenchScript(kind, workspace);
}
let jobsSent = jobs;
pastJobs = await getCompletedJobsCount();
const jobsSent = jobs;
console.log(`Bulk creating ${jobsSent} jobs`);
const start_create = Date.now();
@@ -84,9 +140,8 @@ export async function main({
body = JSON.stringify({
kind: "script",
path: "f/benchmarks/" + kind,
dedicated_worker: kind === "dedicated",
});
} else if (["2steps", "onebranch", "branchallparrallel"].includes(kind)) {
} else if (["2steps"].includes(kind)) {
const payload = getFlowPayload(kind);
body = JSON.stringify({
kind: "flow",
@@ -113,6 +168,7 @@ export async function main({
if (!response.ok) {
throw new Error("Failed to create jobs: " + response.statusText);
}
const uuids = await response.json();
const end_create = Date.now();
const create_duration = end_create - start_create;
console.log(
@@ -122,69 +178,66 @@ export async function main({
);
let start = Date.now();
let queue_length = jobsSent;
let completedJobs = 0;
let lastElapsed = 0;
let lastQueueLength = queue_length;
const updateState = setInterval(async () => {
const elapsed = start ? Date.now() - start : 0;
queue_length = (
await (
await fetch(
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } }
let lastCompletedJobs = 0;
let didStart = false;
while (completedJobs < jobsSent) {
const loopStart = Date.now();
if (!didStart) {
const actual_queue = await getQueueCount();
if (actual_queue < jobsSent) {
start = Date.now();
didStart = true;
}
} else {
const elapsed = start ? Date.now() - start : 0;
completedJobs = await getCompletedJobsCount();
if (kind === "2steps") {
completedJobs = Math.floor(completedJobs / 3);
}
const avgThr = ((completedJobs / elapsed) * 1000).toFixed(2);
const instThr =
lastElapsed > 0
? (
((completedJobs - lastCompletedJobs) / (elapsed - lastElapsed)) *
1000
).toFixed(2)
: 0;
lastElapsed = elapsed;
lastCompletedJobs = completedJobs;
await Deno.stdout.write(
enc(
`elapsed: ${(elapsed / 1000).toFixed(
2
)} | jobs executed: ${completedJobs}/${jobsSent} (thr: inst ${instThr} - avg ${avgThr}) | remaining: ${
jobsSent - completedJobs
} \r`
)
).json()
).database_length;
const avgThr = (((jobsSent - queue_length) / elapsed) * 1000).toFixed(2);
const instThr =
lastElapsed > 0
? (
((lastQueueLength - queue_length) / (elapsed - lastElapsed)) *
1000
).toFixed(2)
: 0;
lastElapsed = elapsed;
lastQueueLength = queue_length;
await Deno.stdout.write(
enc(
`elapsed: ${(elapsed / 1000).toFixed(2)} | jobs executed: ${
jobsSent - queue_length
}/${jobsSent} (thr: inst ${instThr} - avg ${avgThr}) | queue: ${queue_length} \r`
)
);
}, 10);
while (queue_length > 0) {
if (queue_length < jobsSent && jobsSent === jobs) {
// reset start time to when the first job was picked up
start = Date.now();
jobsSent = queue_length;
);
}
const loopDuration = (Date.now() - loopStart) / 1000.0;
if (loopDuration < 0.05) {
await sleep(0.05 - loopDuration);
}
await sleep(0.01);
}
clearInterval(updateState);
const total_duration_sec = (Date.now() - start) / 1000.0;
await sleep(0.1);
console.log(`\njobs: ${jobsSent}`);
console.log(`duration: ${total_duration_sec}s`);
console.log(`avg. throughput (jobs/time): ${jobsSent / total_duration_sec}`);
console.log(
"queue length:",
(
await (
await fetch(
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
{ headers: { ["Authorization"]: "Bearer " + config.token } }
)
).json()
).database_length
);
console.log("completed jobs", completedJobs);
console.log("queue length:", await getQueueCount());
if (!noVerify && kind !== "noop") {
await verifyOutputs(uuids, config.workspace_id);
}
console.log("done");
return {
@@ -229,7 +282,7 @@ if (import.meta.main) {
)
.option(
"--kind <kind:string>",
"Specifiy the benchmark kind among: deno, identity, python, go, bash, dedicated, bun, noop, 2steps, onebranch, branchallparrallel",
"Specifiy the benchmark kind among: deno, identity, python, go, bash, dedicated, bun, noop, 2steps",
{
required: true,
}
@@ -237,6 +290,9 @@ if (import.meta.main) {
.option("-j --jobs <jobs:number>", "Number of jobs to create.", {
default: 10000,
})
.option("--no-verify", "Do not verify the output of the jobs.", {
default: false,
})
.action(main)
.command(
"upgrade",
+21
View File
@@ -23,6 +23,25 @@ type Config = {
];
};
async function warmUp(
host: string,
email: string | undefined,
password: string | undefined,
token: string | undefined,
workspace: string
) {
console.log("%cWarming up...", "font-weight: bold;");
await runBenchmark({
host,
email,
password,
token,
workspace,
kind: "noop",
jobs: 5000,
});
}
async function main({
host,
email,
@@ -47,6 +66,8 @@ async function main({
}
}
await warmUp(host, email, password, token, workspace);
try {
const config = await getConfig(configPath);
for (const benchmark of config.benchmarks) {
+21 -8
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.175.0";
export const VERSION = "v1.176.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
@@ -32,16 +32,16 @@ async function waitForDedicatedWorker(workspace: string, path: string) {
const query = windmill.JobService.runWaitResultScriptByPath({
workspace,
path,
requestBody: {
args: {},
},
requestBody: {},
});
const timeout = new Promise((_, reject) => {
setTimeout(() => {
let timeout;
const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(() => {
reject("Timeout");
}, 15000);
});
await Promise.race([query, timeout]);
await Promise.race([query, timeoutPromise]);
clearTimeout(timeout);
}
export async function createBenchScript(
@@ -63,6 +63,7 @@ export async function createBenchScript(
let scriptContent: string;
let language: string;
let schemaProperties = {};
if (scriptPattern === "python") {
scriptContent =
'import os\n\ndef main():\n return os.environ.get("WM_JOB_ID")';
@@ -74,9 +75,15 @@ export async function createBenchScript(
} else if (scriptPattern === "bash") {
scriptContent = "echo $WM_JOB_ID";
language = "bash";
} else if (scriptPattern === "dedicated" || scriptPattern === "bun") {
} else if (scriptPattern === "bun") {
scriptContent = 'export function main(){ return Bun.env["WM_JOB_ID"]; }';
language = "bun";
} else if (scriptPattern === "dedicated") {
scriptContent = "export function main(uuid){ return uuid; }";
language = "bun";
schemaProperties = {
uuid: { default: null, description: "", type: "string" },
};
} else if (scriptPattern === "deno") {
scriptContent =
'export function main(){ return Deno.env.get("WM_JOB_ID"); }';
@@ -96,6 +103,12 @@ export async function createBenchScript(
description: "",
language: language as api.NewScript.language,
dedicated_worker: scriptPattern === "dedicated",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
properties: schemaProperties,
required: [],
type: "object",
},
},
});
+2 -2
View File
@@ -3,7 +3,7 @@
{
"graph_title": "noop throughput benchmark (single worker)",
"kind": "noop",
"jobs": 5000
"jobs": 30000
},
{
"graph_title": "flow throughput benchmark (single worker)",
@@ -13,7 +13,7 @@
{
"graph_title": "dedicated throughput benchmark (single worker)",
"kind": "dedicated",
"jobs": 2000
"jobs": 30000
},
{
"graph_title": "deno throughput benchmark (single worker)",
+1 -1
View File
@@ -31,7 +31,7 @@ addEventListener("error", (event) => {
}
});
export const VERSION = "v1.175.0";
export const VERSION = "v1.176.0";
let command: any = new Command()
.name("wmill")
+9 -8
View File
@@ -41,12 +41,15 @@ services:
db:
condition: service_healthy
windmill_worker:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 3
resources:
limits:
cpus: "1"
memory: 2048M
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL}
@@ -66,30 +69,28 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- worker_dependency_cache:/tmp/windmill/cache
## This worker is specialized for "native" jobs. They run in-process and can thus be parallelized to more than 1 at a time on a given worker which is why NUM_WORKERS is set to 4
## This worker is specialized for "native" jobs. Native jobs run in-process and thus are much more lightweight than other jobs
windmill_worker_native:
# Use ghcr.io/windmill-labs/windmill-ee:main for the ee
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
replicas: 2
resources:
limits:
cpus: "0.25"
memory: 512M
limits:
cpus: "0.1"
memory: 128M
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL}
- RUST_LOG=info
- DISABLE_SERVER=true
- METRICS_ADDR=false # (ee only, if set to true, metrics will be exposed on port 8001)
- NUM_WORKERS=4
- WORKER_GROUP=native
depends_on:
db:
condition: service_healthy
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest
restart: unless-stopped
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.175.0",
"version": "1.176.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.175.0",
"version": "1.176.0",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.175.0",
"version": "1.176.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+60 -92
View File
@@ -57,12 +57,10 @@
<script lang="ts">
import { oauthStore, workspaceStore } from '$lib/stores'
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import IconedResourceType from './IconedResourceType.svelte'
import { OauthService, ResourceService, VariableService, type TokenResponse } from '$lib/gen'
import { emptyString, truncateRev } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import Path from './Path.svelte'
import { Button, Drawer, Skeleton } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
@@ -71,10 +69,11 @@
import autosize from 'svelte-autosize'
import WhitelistIp from './WhitelistIp.svelte'
import { sendUserToast } from '$lib/toast'
import OauthScopes from './OauthScopes.svelte'
export let newPageOAuth = false
const nativeLanguagesCategory = ['postgresql', 'mysql', 'bigquery', 'snowflake', 'graphql'];
const nativeLanguagesCategory = ['postgresql', 'mysql', 'bigquery', 'snowflake', 'graphql']
let filter = ''
let manual = false
@@ -195,14 +194,14 @@
linkedSecret: undefined
}
])
const filteredNativeLanguages = filteredConnectsManual?.filter(([key, _]) =>
nativeLanguagesCategory.includes(key)
);
const filteredNativeLanguages = filteredConnectsManual?.filter(([key, _]) =>
nativeLanguagesCategory.includes(key)
)
filteredConnectsManual = [
...(filteredNativeLanguages ?? []),
...(filteredConnectsManual ?? []).filter(([key, _]) => !nativeLanguagesCategory.includes(key))
];
filteredConnectsManual = [
...(filteredNativeLanguages ?? []),
...(filteredConnectsManual ?? []).filter(([key, _]) => !nativeLanguagesCategory.includes(key))
]
}
async function next() {
@@ -397,93 +396,62 @@
{#if manual == false && resource_type != ''}
<h3>Scopes</h3>
{#if !manual && resource_type != ''}
{#each scopes as v}
<div class="flex flex-row max-w-md mb-2">
<input type="text" bind:value={v} />
<Button
variant="border"
color="red"
size="xs"
btnClasses="mx-6"
on:click={() => {
scopes = scopes.filter((el) => el != v)
}}
>
<Icon data={faMinus} />
</Button>
</div>
{/each}
<div class="flex items-center mt-1">
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: faPlus }}
on:click={() => {
scopes = scopes.concat('')
}}
>
Add item
</Button>
<span class="ml-2 text-sm text-tertiary">
({(scopes ?? []).length} item{(scopes ?? []).length > 1 ? 's' : ''})
</span>
</div>
<OauthScopes bind:scopes />
{/if}
{/if}
<h2 class="mt-8 mb-4">Others</h2>
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#if filteredConnectsManual}
{#each filteredConnectsManual as [key, _]}
{#if nativeLanguagesCategory.includes(key)}
<Button
size="sm"
variant="border"
color={key === resource_type ? 'blue' : 'light'}
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
manual = true;
resource_type = key;
next();
dispatch('click');
}}
>
<IconedResourceType name={key} after={true} width="20px" height="20px" />
</Button>
{/if}
{/each}
{/if}
</div>
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#if filteredConnectsManual}
{#each filteredConnectsManual as [key, _]}
{#if nativeLanguagesCategory.includes(key)}
<Button
size="sm"
variant="border"
color={key === resource_type ? 'blue' : 'light'}
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
manual = true
resource_type = key
next()
dispatch('click')
}}
>
<IconedResourceType name={key} after={true} width="20px" height="20px" />
</Button>
{/if}
{/each}
{/if}
</div>
<h2 class="mt-8 mb-4"></h2>
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#if filteredConnectsManual}
{#each filteredConnectsManual as [key, _]}
{#if !nativeLanguagesCategory.includes(key)} <!-- Exclude specific items -->
<Button
size="sm"
variant="border"
color={key === resource_type ? 'blue' : 'light'}
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
manual = true;
resource_type = key;
next();
dispatch('click');
}}
>
<IconedResourceType name={key} after={true} width="20px" height="20px" />
</Button>
{/if}
{/each}
{:else}
{#each new Array(9) as _}
<Skeleton layout={[[2]]} />
{/each}
{/if}
</div>
<h2 class="mt-8 mb-4" />
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#if filteredConnectsManual}
{#each filteredConnectsManual as [key, _]}
{#if !nativeLanguagesCategory.includes(key)}
<!-- Exclude specific items -->
<Button
size="sm"
variant="border"
color={key === resource_type ? 'blue' : 'light'}
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
manual = true
resource_type = key
next()
dispatch('click')
}}
>
<IconedResourceType name={key} after={true} width="20px" height="20px" />
</Button>
{/if}
{/each}
{:else}
{#each new Array(9) as _}
<Skeleton layout={[[2]]} />
{/each}
{/if}
</div>
{:else if step == 2 && manual}
<Path
bind:error={pathError}
+6 -1
View File
@@ -114,7 +114,10 @@
}
function evalValueToRaw() {
rawValue = inputCat === 'object' ? JSON.stringify(value, null, 2) : undefined
rawValue =
inputCat === 'object' || inputCat === 'resource-object'
? JSON.stringify(value, null, 2)
: undefined
}
evalValueToRaw()
@@ -351,6 +354,8 @@
</Button>
</div>
</div>
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
<span class="text-2xs text-tertiary">Loading resource types...</span>
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format.split('-').length > 1 && resourceTypes.includes(format.substring('resource-'.length))))}
<ObjectResourceInput {disablePortal} {format} bind:value {showSchemaExplorer} />
{:else if inputCat == 'object' || inputCat == 'resource-object'}
@@ -0,0 +1,76 @@
<script lang="ts">
import OauthExtraParams from './OauthExtraParams.svelte'
import OauthScopes from './OauthScopes.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
export let connect_config = {
scopes: [],
auth_url: '',
token_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}
$: if (!connect_config) {
connect_config = {
scopes: [],
auth_url: '',
token_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}
}
</script>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Auth URL</span>
<input
type="text"
placeholder="https://github.com/login/oauth/authorize"
bind:value={connect_config.auth_url}
/>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Token URL</span>
<input
type="text"
placeholder="https://github.com/login/oauth/access_token"
bind:value={connect_config.token_url}
/>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Scopes</span>
<OauthScopes bind:scopes={connect_config.scopes} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Extra Query Args for Authorize Request&nbsp;<Tooltip
>Not needed in most cases. Examples of uses: google apis require the 2 extra args
"access_type=offline&prompt=consent"</Tooltip
></span
>
<OauthExtraParams bind:extra_params={connect_config.extra_params} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Extra Query Args for Token request <Tooltip>Not needed in most cases</Tooltip></span
>
<OauthExtraParams bind:extra_params={connect_config.extra_params_callback} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Payload <Tooltip
>Auth is passed in query most commonly. LinkedIn is an example of OAuth using
x-www-form-urlencoded
</Tooltip></span
>
<div>
<Toggle
options={{ left: 'in query args', right: 'in body x-www-form-urlencoded' }}
bind:checked={connect_config.req_body_auth}
/></div
>
</label>
@@ -421,7 +421,7 @@
let abortController: AbortController | undefined = undefined
let copilotLoading = false
let flowCopilotMode: 'trigger' | 'sequence' = 'trigger'
let flowCopilotMode: 'trigger' | 'sequence' = 'sequence'
let copilotStatus: string = ''
let copilotFlowInputs: Record<string, SchemaProperty> = {}
let copilotFlowRequiredInputs: string[] = []
@@ -34,6 +34,9 @@
return modules
.filter((x) => idOrders.indexOf(x.id) <= upTo)
.map((m) => {
if (idOrders.indexOf(m.id) == upTo) {
return m
}
if (m.value.type === 'forloopflow') {
m.value.modules = sliceModules(m.value.modules, upTo, idOrders)
} else if (m.value.type === 'branchone') {
@@ -15,11 +15,11 @@
import type VariableEditor from './VariableEditor.svelte'
import type ItemPicker from './ItemPicker.svelte'
import { ResourceService, type InputTransform } from '$lib/gen'
import type { InputTransform } from '$lib/gen'
import TemplateEditor from './TemplateEditor.svelte'
import { setInputCat as computeInputCat, isCodeInjection } from '$lib/utils'
import { FunctionSquare, Plug } from 'lucide-svelte'
import { workspaceStore } from '$lib/stores'
import { getResourceTypes } from './resourceTypesStore'
export let schema: Schema
export let arg: InputTransform | any
@@ -141,8 +141,9 @@
let resourceTypes: string[] | undefined = undefined
async function loadResourceTypes() {
resourceTypes = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! })
resourceTypes = await getResourceTypes()
}
loadResourceTypes()
</script>
@@ -263,7 +264,7 @@
</span>
{/if}
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
<div class="mt-2 min-h-[28px] rounded border border-1 border-gray-400">
<div class="mt-2 min-h-[28px]">
{#if arg}
<TemplateEditor
bind:this={monacoTemplate}
@@ -14,6 +14,9 @@
import KeycloakSetting from './KeycloakSetting.svelte'
import Alert from './common/alert/Alert.svelte'
import { isCloudHosted } from '$lib/cloud'
import { capitalize } from '$lib/utils'
import { enterpriseLicense } from '$lib/stores'
import CustomOauth from './CustomOauth.svelte'
export const settings: Record<string, Setting[]> = {
Core: [
@@ -50,6 +53,14 @@
fieldType: 'seconds',
placeholder: '60',
storage: 'config'
},
{
label: 'License Key',
description: 'License Key required to use the EE (switch image for windmill-ee)',
key: 'license_key',
fieldType: 'license_key',
placeholder: 'only needed to prepare upgrade to EE',
storage: 'setting'
}
],
SMTP: [
@@ -117,7 +128,7 @@
}
}
initialOauths = (await SettingService.getGlobal({ key: 'oauths' })) ?? {}
oauths = { ...initialOauths }
oauths = JSON.parse(JSON.stringify(initialOauths))
initialValues = Object.fromEntries(
(
await Promise.all(
@@ -128,7 +139,7 @@
)
).flat()
)
values = { ...initialValues }
values = JSON.parse(JSON.stringify(initialValues))
if (values['retention_period_secs'] == undefined) {
values['retention_period_secs'] = 60 * 60 * 24 * 60
}
@@ -150,7 +161,7 @@
name: 'server',
requestBody: newServerConfig
})
serverConfig = { ...newServerConfig }
serverConfig = JSON.parse(JSON.stringify(newServerConfig))
}
await Promise.all(
allSettings
@@ -166,7 +177,7 @@
await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } })
})
)
initialValues = { ...values }
initialValues = JSON.parse(JSON.stringify(initialValues))
if (!deepEqual(initialOauths, oauths)) {
await SettingService.setGlobal({
@@ -175,7 +186,7 @@
value: oauths
}
})
initialOauths = { ...oauths }
initialOauths = JSON.parse(JSON.stringify(oauths))
}
} else {
console.error('Values not loaded')
@@ -187,7 +198,36 @@
let resourceName = ''
let tab: 'Core' | 'SMTP' | 'OAuth' = 'Core'
function parseDate(license_key: string): string | undefined {
let splitted = license_key.split('.')
if (splitted.length >= 3) {
try {
let i = parseInt(splitted[1])
let date = new Date(i * 1000)
return date.toDateString()
} catch {}
}
return undefined
}
let to: string = ''
const windmillBuiltins = [
'github',
'gitlab',
'bitbucket',
'slack',
'gsheets',
'gdrive',
'gmail',
'gcal',
'gcloud',
'gworkspace',
'basecamp',
'linkedin'
]
let oauth_name = 'custom'
</script>
<div class="pb-8">
@@ -226,6 +266,37 @@
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'textarea'}
<textarea
rows="2"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
{:else if setting.fieldType == 'license_key'}
<div class="flex justify-between gap-2">
<textarea
rows="2"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
/>
<Button
variant={values[setting.key] ? 'contained' : 'border'}
size="xs"
on:click={async () => {
await SettingService.testLicenseKey({
requestBody: { license_key: values[setting.key] }
})
sendUserToast('Valid key')
}}>Test Key</Button
>
</div>
{#if values[setting.key]?.length > 0}
{#if parseDate(values[setting.key])}
<span class="text-gray-600 text-2xs"
>License key expires on {parseDate(values[setting.key])}</span
>
{/if}
{/if}
{:else if setting.fieldType == 'email'}
<input
type="email"
@@ -293,6 +364,11 @@
<Alert type="warning" title="Limited to 50 SSO users">
Without EE, the number of SSO users is limited to 50. SCIM/SAML is available on EE
</Alert>
<div class="py-1" />
<Alert type="info" title="Test on a separate tab">
The recommended workflow is to to save your oauth setting and test them directly on the
login or resource page
</Alert>
<div class="flex flex-col gap-2 py-4">
<OAuthSetting name="google" bind:value={oauths['google']} />
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
@@ -303,6 +379,11 @@
<KeycloakSetting bind:value={oauths['keycloak']} />
</div>
<h4 class="py-4">OAuth</h4>
<Alert type="info" title="Require a corresponding resource type">
After setting an oauth client, make sure that there is a corresponding resource type
with the same name with a "token" field in the admins workspace.
</Alert>
<div class="py-1" />
<OAuthSetting login={false} name="slack" bind:value={oauths['slack']} />
<div class="py-1" />
@@ -311,7 +392,7 @@
{#if oauths[k]}
<div class="flex flex-col gap-2 pb-4">
<div class="flex flex-row items-center gap-2">
<label class="text-md font-medium text-gray-700">{k}</label>
<label class="text-md font-medium text-primary">{k}</label>
<CloseButton
on:close={() => {
delete oauths[k]
@@ -332,6 +413,9 @@
bind:value={oauths[k]['secret']}
/>
</label>
{#if !windmillBuiltins.includes(k) && k != 'slack'}
<CustomOauth bind:connect_config={oauths[k]['connect_config']} />
{/if}
</div>
</div>
{/if}
@@ -339,20 +423,28 @@
{/each}
<div class="flex gap-2">
<input type="text" placeholder="slack" bind:value={resourceName} />
<select name="oauth_name" id="oauth_name" bind:value={oauth_name}>
<option value="custom">Fully Custom (require ee)</option>
{#each windmillBuiltins as name}
<option value={name}>{capitalize(name)}</option>
{/each}
</select>
<input type="text" placeholder="client_id" bind:value={resourceName} />
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: faPlus }}
disabled={resourceName == ''}
disabled={(oauth_name == 'custom' && resourceName == '') ||
(oauth_name == 'custom' && !$enterpriseLicense)}
on:click={() => {
oauths[resourceName] = { id: '', secret: '' }
let name = oauth_name == 'custom' ? resourceName : oauth_name
oauths[name] = { id: '', secret: '' }
resourceName = ''
}}
>
Add OAuth client
Add OAuth client {oauth_name == 'custom' && !$enterpriseLicense ? '(require ee)' : ''}
</Button>
</div>
</div>
@@ -31,7 +31,7 @@
</script>
<div class="flex flex-col gap-1">
<label class="text-sm font-medium text-gray-700 flex gap-4 items-center"
<label class="text-sm font-medium text-primary flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name={'keycloak'} after={true} /></div><Toggle
checked={enabled}
on:change={(e) => {
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type Job, Script } from '$lib/gen'
import { ScriptService, type FlowModule, type Job, Script, JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getModifierKey } from '$lib/utils'
import { getScriptByPath } from '$lib/scripts'
@@ -71,8 +71,12 @@
args,
$flowStore?.tag ?? script.tag
)
} else if (val.type == 'flow') {
await testJobLoader?.abstractRun(() =>
JobService.runFlowByPath({ workspace: $workspaceStore!, path: val.path, requestBody: args })
)
} else {
throw Error('not testable module type')
throw Error('Not supported module type')
}
}
@@ -8,10 +8,10 @@
import { getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { evalValue } from './flows/utils'
import { ResourceService, type FlowModule } from '$lib/gen'
import type { FlowModule } from '$lib/gen'
import type { PickableProperties } from './flows/previousResults'
import type SimpleEditor from './SimpleEditor.svelte'
import { workspaceStore } from '$lib/stores'
import { getResourceTypes } from './resourceTypesStore'
export let schema: Schema
export let args: Record<string, any> = {}
@@ -63,8 +63,9 @@
let resourceTypes: string[] | undefined = undefined
async function loadResourceTypes() {
resourceTypes = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! })
resourceTypes = await getResourceTypes()
}
loadResourceTypes()
</script>
+16 -13
View File
@@ -7,15 +7,13 @@
export let value: any
export let login = true
$: if (value && value?.['allowed_domains'] == undefined) {
value = { ...(value ?? {}), allowed_domains: [] }
}
$: enabled = value != undefined
let allowed_domains = value?.['allowed_domains'] ?? ''
</script>
<div class="flex flex-col gap-1">
<label class="text-sm flex gap-4 items-center font-medium text-gray-700"
<label class="text-sm flex gap-4 items-center font-medium text-primary"
><div class="w-[120px]"><IconedResourceType {name} after={true} /></div><Toggle
checked={enabled}
on:change={(e) => {
@@ -39,14 +37,19 @@
</label>
{#if login}
<label class="block pb-2">
{#if value['allowed_domains']}
<span class="text-primary font-semibold text-sm">Allowed domain</span>
<input
type="text"
placeholder="windmill.dev"
bind:value={value['allowed_domains'][0]}
/>
{/if}
<span class="text-primary font-semibold text-sm">Allowed domain</span>
<input
type="text"
placeholder="windmill.dev"
bind:value={allowed_domains}
on:keyup={() => {
if (allowed_domains == '') {
value['allowed_domains'] = undefined
} else {
value['allowed_domains'] = [allowed_domains]
}
}}
/>
</label>
{/if}
{#if name == 'google'}
@@ -0,0 +1,50 @@
<script lang="ts">
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Button } from './common'
export let extra_params: Record<string, string> = {}
let extra_params_vec: [string, string][] = Object.entries(extra_params)
function sync() {
extra_params = Object.fromEntries(extra_params_vec)
}
</script>
{#each extra_params_vec as o}
<div class="flex flex-row max-w-md mb-2">
<input type="text" on:keyup={sync} bind:value={o[0]} />
<input type="text" on:keyup={sync} bind:value={o[1]} />
<Button
variant="border"
color="red"
size="xs"
btnClasses="mx-6"
on:click={() => {
extra_params_vec = extra_params_vec.filter((e) => e[0] != o[0])
sync()
}}
>
<Icon data={faMinus} />
</Button>
</div>
{/each}
<div class="flex items-center mt-1">
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: faPlus }}
on:click={() => {
extra_params_vec = extra_params_vec.concat([['key', 'value']])
sync()
}}
>
Add item
</Button>
<span class="ml-2 text-sm text-tertiary">
({(extra_params_vec ?? []).length} item{(extra_params_vec ?? []).length > 1 ? 's' : ''})
</span>
</div>
@@ -0,0 +1,41 @@
<script lang="ts">
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Button } from './common'
export let scopes: string[] = []
</script>
{#each scopes as v}
<div class="flex flex-row max-w-md mb-2">
<input type="text" bind:value={v} />
<Button
variant="border"
color="red"
size="xs"
btnClasses="mx-6"
on:click={() => {
scopes = scopes.filter((el) => el != v)
}}
>
<Icon data={faMinus} />
</Button>
</div>
{/each}
<div class="flex items-center mt-1">
<Button
variant="border"
color="blue"
hover="yo"
size="sm"
endIcon={{ icon: faPlus }}
on:click={() => {
scopes = scopes.concat('')
}}
>
Add item
</Button>
<span class="ml-2 text-sm text-tertiary">
({(scopes ?? []).length} item{(scopes ?? []).length > 1 ? 's' : ''})
</span>
</div>
@@ -33,7 +33,7 @@
</script>
<div class="flex flex-col gap-1">
<label class="text-sm font-medium text-gray-700 flex gap-4 items-center"
<label class="text-sm font-medium text-primary flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name="okta" after={true} /></div><Toggle
checked={enabled}
on:change={(e) => {
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ResourceService, VariableService } from '$lib/gen'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
@@ -10,6 +10,7 @@
import VariableEditor from './VariableEditor.svelte'
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
import { getResourceTypes } from './resourceTypesStore'
export let schema: Schema | any
export let args: Record<string, any> = {}
@@ -70,8 +71,9 @@
let resourceTypes: string[] | undefined = undefined
async function loadResourceTypes() {
resourceTypes = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! })
resourceTypes = await getResourceTypes()
}
loadResourceTypes()
</script>
+329 -320
View File
@@ -8,9 +8,8 @@
import { emptySchema, encodeState, getModifierKey } from '$lib/utils'
import Path from './Path.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Alert, Badge, Button, Drawer, Kbd, SecondsInput } from './common'
import { Alert, Badge, Button, Drawer, Kbd, SecondsInput, Tab, TabContent, Tabs } from './common'
import { faPlus, faSave } from '@fortawesome/free-solid-svg-icons'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
@@ -19,7 +18,7 @@
import { Loader2, Pen, X } from 'lucide-svelte'
import autosize from 'svelte-autosize'
import type Editor from './Editor.svelte'
import { SCRIPT_SHOW_BASH, SCRIPT_SHOW_GO, SCRIPT_CUSTOMISE_SHOW_KIND } from '$lib/consts'
import { SCRIPT_SHOW_BASH, SCRIPT_SHOW_GO } from '$lib/consts'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { sendUserToast } from '$lib/toast'
import { isCloudHosted } from '$lib/cloud'
@@ -28,6 +27,7 @@
import { fade } from 'svelte/transition'
import Popover from './Popover.svelte'
import Toggle from './Toggle.svelte'
import ScriptSchema from './ScriptSchema.svelte'
export let script: NewScript
export let initialPath: string = ''
@@ -41,7 +41,6 @@
(initialPath == '' &&
$page.url.searchParams.get('state') == undefined &&
$page.url.searchParams.get('collab') == undefined)
let advancedOpen = false
let editor: Editor | undefined = undefined
let scriptEditor: ScriptEditor | undefined = undefined
@@ -272,108 +271,134 @@
let path: Path | undefined = undefined
let dirtyPath = false
let selectedTab: 'metadata' | 'runtime' | 'ui' = 'metadata'
</script>
<svelte:window on:keydown={onKeyDown} />
<UnsavedConfirmationModal />
{#if !$userStore?.operator}
<Drawer placement="right" bind:open={metadataOpen} size="800px">
<DrawerContent title="Metadata" on:close={() => (metadataOpen = false)}>
<h2 class="border-b pb-1 mb-4">Summary</h2>
<input
type="text"
autofocus
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
on:keyup={() => {
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
path?.setName(
script.summary
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/-+/g, '_')
.replace(/^-|-$/g, '')
)
}
}}
/>
<h2 class="border-b pb-1 mt-10 mb-4">Path</h2>
<Path
bind:this={path}
bind:error={pathError}
bind:path={script.path}
bind:dirty={dirtyPath}
{initialPath}
autofocus={false}
namePlaceholder="script"
kind="script"
/>
<h2 class="border-b pb-1 mt-10 mb-4">Language</h2>
{#if lockedLanguage}
<div class="text-sm text-tertiary italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class=" grid grid-cols-3 gap-2">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Popover disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}>
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75' : 'm-[1px]'}
on:click={() => {
template = 'script'
initContent(lang, script.kind, template)
script.language = lang
}}
disabled={lockedLanguage || (enterpriseLangs.includes(lang) && !$enterpriseLicense)}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
</Button>
<svelte:fragment slot="text"
>{label} is only available with an enterprise license</svelte:fragment
>
</Popover>
{/each}
<Button
size="sm"
variant="border"
color={template == 'docker' ? 'blue' : 'light'}
btnClasses={template == 'docker'
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
disabled={lockedLanguage}
on:click={() => {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
}
}
]
)
return
}
template = 'docker'
initContent(Script.language.BASH, script.kind, template)
script.language = Script.language.BASH
}}
<DrawerContent noPadding title="Settings" on:close={() => (metadataOpen = false)}>
<Tabs bind:selected={selectedTab}>
<Tab value="metadata">Metadata</Tab>
<Tab value="runtime">Runtime</Tab>
<Tab value="ui"
>Generated UI<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
>The arguments are synced with the main signature but you may refine the parts that
cannot be inferred from the type directly.</Tooltip
></Tab
>
<LanguageIcon lang="docker" /><span class="ml-2 py-2">Docker</span>
</Button>
<svelte:fragment slot="content">
<div class="px-4">
<TabContent value="metadata">
<div class="text-sm mt-4 text-secondary font-semibold">Summary</div>
<!-- <Button
<input
type="text"
autofocus
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
on:keyup={() => {
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
path?.setName(
script.summary
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/-+/g, '_')
.replace(/^-|-$/g, '')
)
}
}}
/>
<div class="text-sm mt-6 text-secondary font-semibold">Path</div>
<Path
bind:this={path}
bind:error={pathError}
bind:path={script.path}
bind:dirty={dirtyPath}
{initialPath}
autofocus={false}
namePlaceholder="script"
kind="script"
/>
<div class="text-sm mt-6 text-secondary font-semibold">Description</div>
<textarea
use:autosize
bind:value={script.description}
placeholder="Description displayed in the details page"
class="text-sm"
/>
<div class="border-b pb-1 mb-4 mt-8 font-semibold">Language</div>
{#if lockedLanguage}
<div class="text-sm text-tertiary italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class=" grid grid-cols-3 gap-2">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Popover disablePopup={!enterpriseLangs.includes(lang) || !!$enterpriseLicense}>
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses={isPicked
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
on:click={() => {
template = 'script'
initContent(lang, script.kind, template)
script.language = lang
}}
disabled={lockedLanguage ||
(enterpriseLangs.includes(lang) && !$enterpriseLicense)}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2 truncate">{label}</span>
</Button>
<svelte:fragment slot="text"
>{label} is only available with an enterprise license</svelte:fragment
>
</Popover>
{/each}
<Button
size="sm"
variant="border"
color={template == 'docker' ? 'blue' : 'light'}
btnClasses={template == 'docker'
? '!border-2 !bg-blue-50/75 dark:!bg-frost-900/75'
: 'm-[1px]'}
disabled={lockedLanguage}
on:click={() => {
if (isCloudHosted()) {
sendUserToast(
'You cannot use Docker scripts on the multi-tenant platform. Use a dedicated instance or self-host windmill instead.',
true,
[
{
label: 'Learn more',
callback: () => {
window.open('https://www.windmill.dev/docs/advanced/docker', '_blank')
}
}
]
)
return
}
template = 'docker'
initContent(Script.language.BASH, script.kind, template)
script.language = Script.language.BASH
}}
>
<LanguageIcon lang="docker" /><span class="ml-2 py-2">Docker</span>
</Button>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
@@ -386,174 +411,213 @@
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
<h2 class="border-b pb-1 mt-10 mb-4">Description</h2>
<textarea
use:autosize
bind:value={script.description}
placeholder="Description displayed in the details page"
class="text-sm"
/>
<h2 class="border-b pb-1 mt-10 mb-4">Concurrency limits</h2>
<div class="flex gap-x-4 shrink">
<label class="block shrink min-w-0">
<span class="text-secondary text-sm">Maximum number of runs</span>
<input class="!w-55" type="number" bind:value={script.concurrent_limit} />
</label>
<label class="block shrink min-w-0">
<span class="text-secondary text-sm">Per time window (seconds)</span>
<input class="!w-18" type="number" bind:value={script.concurrency_time_window_s} />
</label>
</div>
<h2 class="border-b pb-1 mt-10 mb-4"
>Worker group tag <Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>The script will be executed on a worker configured to accept its worker group tag. For
instance, you could setup an "highmem", or "gpu" worker group.</Tooltip
></h2
>
{#if $workerTags}
{#if $workerTags?.length > 0}
<div class="max-w-sm">
<select
bind:value={script.tag}
on:change={(e) => {
if (script.tag == '') {
script.tag = undefined
}
}}
>
{#if script.tag}
<option value="">reset to default</option>
{:else}
<option value="" disabled selected>Worker Group</option>
{/if}
{#each $workerTags ?? [] as tag (tag)}
<option value={tag}>{tag}</option>
{/each}
</select>
</div>
{:else}
<div class="text-sm text-secondary italic mb-2">
No custom worker group defined on this instance. See <a
href="https://www.windmill.dev/docs/core_concepts/worker_groups"
target="_blank">documentation</a
>
</div>
{/if}
{:else}
<Loader2 class="animate-spin" />
{/if}
<h2 class="border-b pb-1 mt-10 mb-4 flex items-center gap-4"
>Cache <Toggle
size="xs"
checked={Boolean(script.cache_ttl)}
on:change={() => {
if (script.cache_ttl && script.cache_ttl != undefined) {
script.cache_ttl = undefined
} else {
script.cache_ttl = 300
}
}}
options={{
right: 'Cache the results for each possible inputs'
}}
/></h2
>
<div class="flex gap-x-4 shrink flex-col">
<div class="text-xs">How long to the keep cache valid</div>
{#if script.cache_ttl}
<SecondsInput bind:seconds={script.cache_ttl} />
{:else}
<SecondsInput disabled />
{/if}
</div>
<h2 class="border-b pb-1 mt-10 mb-4 items-center flex gap-1"
>Dedicated Workers<Tooltip
>In this mode, the script is meant to be run on dedicated workers that run the script at
native speed. Can reach >400rps per dedicated worker. Only available on enterprise edition
and for the Bun language.</Tooltip
>
<Toggle
disabled={!$enterpriseLicense ||
isCloudHosted() ||
script.language != Script.language.BUN}
size="xs"
checked={Boolean(script.dedicated_worker)}
on:change={() => {
if (script.dedicated_worker) {
script.dedicated_worker = undefined
} else {
script.dedicated_worker = true
}
}}
options={{
right: 'Script is run on dedicated workers'
}}
/></h2
>
{#if !isCloudHosted()}
<h2 class="border-b pb-1 mt-10 mb-4">
Custom env variables
<Tooltip
documentationLink="https://www.windmill.dev/docs/reference#custom-environment-variables"
>
Additional static custom env variables to pass to the script.
</Tooltip>
</h2>
<div class="w-full">
<span class="text-tertiary text-xs pb-2">Format is: `{'<KEY>=<VALUE>'}`</span>
{#if Array.isArray(script.envs ?? [])}
{#each script.envs ?? [] as v, i}
<div class="flex max-w-md mt-1 w-full items-center">
<input type="text" bind:value={v} placeholder="<KEY>=<VALUE>" />
<button
transition:fade|local={{ duration: 50 }}
class="rounded-full p-1 bg-surface/60 duration-200 hover:bg-gray-200"
aria-label="Clear"
on:click={() => {
script.envs && script.envs.splice(i, 1)
script.envs = script.envs
}}
>
<X size={14} />
</button>
</div>
{/each}
{#if script.envs && script.envs.length > 0}
<div class="pt-2" />
<Alert type="warning" title="Not passed in previews"
>Static envs variables are not passed in preview but solely on deployed scripts.</Alert
<div class="border-b pb-1 mb-4 mt-6 font-semibold">
Script kind
<Tooltip>
Tag this script's purpose within flows such that it is available as the
corresponding action.
</Tooltip>
</div>
<div class="flex flex-wrap gap-2 mb-8">
{#each scriptKindOptions as { value, title, desc, documentationLink }}
{@const isPicked = script.kind === value}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'light'}
btnClasses="font-medium {isPicked
? '!bg-blue-50/75 dark:!bg-frost-900/75'
: ''}"
on:click={() => {
template = 'script'
script.kind = value
initContent(script.language, value, template)
}}
>
{title}
{#if desc}
<Tooltip {documentationLink}>
{desc}
</Tooltip>
{/if}
</Button>
{/each}
</div>
</TabContent>
<TabContent value="runtime">
<h2 class="border-b pb-1 mt-4 mb-4">Concurrency limits</h2>
<div class="flex gap-x-4 shrink">
<label class="block shrink min-w-0">
<span class="text-secondary text-sm">Maximum number of runs</span>
<input class="!w-55" type="number" bind:value={script.concurrent_limit} />
</label>
<label class="block shrink min-w-0">
<span class="text-secondary text-sm">Per time window (seconds)</span>
<input
class="!w-18"
type="number"
bind:value={script.concurrency_time_window_s}
/>
</label>
</div>
<h2 class="border-b pb-1 mt-10 mb-4"
>Worker group tag <Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups"
>The script will be executed on a worker configured to accept its worker group
tag. For instance, you could setup an "highmem", or "gpu" worker group.</Tooltip
></h2
>
{/if}
{/if}
</div>
<div class="flex mt-2">
<Button
variant="border"
color="dark"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (script.envs == undefined || !Array.isArray(script.envs)) {
script.envs = []
}
script.envs = script.envs.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
</div>
{/if}
{#if $workerTags}
{#if $workerTags?.length > 0}
<div class="max-w-sm">
<select
bind:value={script.tag}
on:change={(e) => {
if (script.tag == '') {
script.tag = undefined
}
}}
>
{#if script.tag}
<option value="">reset to default</option>
{:else}
<option value="" disabled selected>Worker Group</option>
{/if}
{#each $workerTags ?? [] as tag (tag)}
<option value={tag}>{tag}</option>
{/each}
</select>
</div>
{:else}
<div class="text-sm text-secondary italic mb-2">
No custom worker group defined on this instance. See <a
href="https://www.windmill.dev/docs/core_concepts/worker_groups"
target="_blank">documentation</a
>
</div>
{/if}
{:else}
<Loader2 class="animate-spin" />
{/if}
<h2 class="border-b pb-1 mt-10 mb-4 flex items-center gap-4"
>Cache <Toggle
size="xs"
checked={Boolean(script.cache_ttl)}
on:change={() => {
if (script.cache_ttl && script.cache_ttl != undefined) {
script.cache_ttl = undefined
} else {
script.cache_ttl = 300
}
}}
options={{
right: 'Cache the results for each possible inputs'
}}
/></h2
>
<div class="flex gap-x-4 shrink flex-col">
<div class="text-xs">How long to the keep cache valid</div>
{#if script.cache_ttl}
<SecondsInput bind:seconds={script.cache_ttl} />
{:else}
<SecondsInput disabled />
{/if}
</div>
<h2 class="border-b pb-1 mt-10 mb-4 items-center flex gap-1"
>Dedicated Workers<Tooltip
>In this mode, the script is meant to be run on dedicated workers that run the
script at native speed. Can reach >1500rps per dedicated worker. Only available on
enterprise edition and for the Bun language.</Tooltip
>
<Toggle
disabled={!$enterpriseLicense ||
isCloudHosted() ||
script.language != Script.language.BUN}
size="xs"
checked={Boolean(script.dedicated_worker)}
on:change={() => {
if (script.dedicated_worker) {
script.dedicated_worker = undefined
} else {
script.dedicated_worker = true
}
}}
options={{
right: 'Script is run on dedicated workers'
}}
/></h2
>
{#if !isCloudHosted()}
<h2 class="border-b pb-1 mt-10 mb-4">
Custom env variables
<Tooltip
documentationLink="https://www.windmill.dev/docs/reference#custom-environment-variables"
>
Additional static custom env variables to pass to the script.
</Tooltip>
</h2>
<div class="w-full">
<span class="text-tertiary text-xs pb-2">Format is: `{'<KEY>=<VALUE>'}`</span>
{#if Array.isArray(script.envs ?? [])}
{#each script.envs ?? [] as v, i}
<div class="flex max-w-md mt-1 w-full items-center">
<input type="text" bind:value={v} placeholder="<KEY>=<VALUE>" />
<button
transition:fade|local={{ duration: 50 }}
class="rounded-full p-1 bg-surface/60 duration-200 hover:bg-gray-200"
aria-label="Clear"
on:click={() => {
script.envs && script.envs.splice(i, 1)
script.envs = script.envs
}}
>
<X size={14} />
</button>
</div>
{/each}
{#if script.envs && script.envs.length > 0}
<div class="pt-2" />
<Alert type="warning" title="Not passed in previews"
>Static envs variables are not passed in preview but solely on deployed
scripts.</Alert
>
{/if}
{/if}
</div>
<div class="flex mt-2">
<Button
variant="border"
color="dark"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (script.envs == undefined || !Array.isArray(script.envs)) {
script.envs = []
}
script.envs = script.envs.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
</div>
{/if}
</TabContent>
<TabContent value="ui">
<div class="mt-4" />
<ScriptSchema bind:schema={script.schema} />
</TabContent>
</div>
</svelte:fragment>
</Tabs>
</DrawerContent>
</Drawer>
@@ -618,17 +682,7 @@
metadataOpen = true
}}
>
Metadata
</Button>
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
advancedOpen = true
}}
>
Customise
Settings
</Button>
<Button
loading={loadingDraft}
@@ -654,51 +708,6 @@
</div>
</div>
<Drawer bind:open={advancedOpen} size="800px">
<DrawerContent title="Customise" on:close={() => (advancedOpen = false)}>
{#if SCRIPT_CUSTOMISE_SHOW_KIND}
<h2 class="border-b pb-1 mb-4">
Script kind &nbsp;
<Tooltip>
Tag this script's purpose within flows such that it is available as the corresponding
action.
</Tooltip>
</h2>
<div class="flex flex-wrap gap-2">
{#each scriptKindOptions as { value, title, desc, documentationLink }}
{@const isPicked = script.kind === value}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses="font-medium {isPicked ? '!bg-blue-50/75 !dark:bg-blue-900/75' : ''}"
on:click={() => {
template = 'script'
script.kind = value
initContent(script.language, value, template)
}}
>
{title}
{#if desc}
<Tooltip {documentationLink}>
{desc}
</Tooltip>
{/if}
</Button>
{/each}
</div>
{/if}
<h2 class="border-b pb-1 mt-10 mb-4"
>Arguments &nbsp;<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
>The arguments are synced with the main signature but you may refine the parts that
cannot be inferred from the type directly.</Tooltip
></h2
>
<ScriptSchema bind:schema={script.schema} />
</DrawerContent>
</Drawer>
<ScriptEditor
collabMode
edit={initialPath != ''}
@@ -604,7 +604,7 @@
<div
bind:this={divEl}
style="height: 18px;"
class="{$$props.class ?? ''} template rounded-lg min-h-4 mx-0.5 overflow-clip"
class="{$$props.class ?? ''} border template rounded min-h-4 mx-0.5 overflow-clip"
bind:clientWidth={width}
/>
+27 -26
View File
@@ -18,7 +18,7 @@
export let size: 'sm' | 'xs' = 'sm'
const id = (Math.random() + 1).toString(36).substring(10)
const dispatch = createEventDispatcher()
const bothOptions = Boolean(options.left) && Boolean(options.right);
const bothOptions = Boolean(options.left) && Boolean(options.right)
</script>
<span class="{$$props.class} z-auto">
@@ -29,20 +29,21 @@
: 'cursor-pointer'}"
>
{#if Boolean(options?.left)}
<span
class={twMerge(
'mr-2 font-medium duration-50',
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.left}
</span>
<span
class={twMerge(
'mr-2 font-medium duration-50',
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.left}
</span>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="relative" on:click|stopPropagation>
<input
on:focus
@@ -67,20 +68,20 @@
/>
</div>
{#if Boolean(options?.right)}
<span
class={twMerge(
'ml-2 font-medium duration-50',
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
<span
class={twMerge(
'ml-2 font-medium duration-50',
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
{/if}
</label>
</span>
@@ -70,6 +70,7 @@
'h-full w-full overflow-y-auto prose',
resolvedConfig?.size ? proseMapping[resolvedConfig.size] : '',
css?.container?.class,
' dark:prose-invert',
'wm-markdown'
)}
style={css?.container?.style}
@@ -165,7 +165,7 @@
}
const headers = Array.from(
new Set(result.flatMap((row) => (typeof row == 'object' ? Object.keys(row) : [])))
new Set(result.flatMap((row) => (typeof row == 'object' ? Object.keys(row ?? {}) : [])))
)
$options = {
@@ -97,7 +97,7 @@
}
}
$: if (render) {
$: if (render && portalRef && outerDiv && items?.length > 0) {
tick().then(() => {
moveOptionsToPortal()
})
@@ -106,7 +106,7 @@
<InputValue key="disable" {id} input={disableTab} bind:value={resolvedDisabledTabs[index]} />
{/each}
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full' : 'w-full'}>
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full h-full' : 'w-full'}>
{#if !resolvedConfig.tabsKind || resolvedConfig.tabsKind == 'tabs' || (resolvedConfig.tabsKind == 'invisibleOnView' && $mode == 'dnd')}
<div bind:clientHeight={tabHeight}>
<Tabs
@@ -36,7 +36,7 @@
import VariableEditor from '$lib/components/VariableEditor.svelte'
import { VariableService, type Policy } from '$lib/gen'
import { initHistory } from '$lib/history'
import { Component, Paintbrush, Plus } from 'lucide-svelte'
import { Component, Minus, Paintbrush, Plus } from 'lucide-svelte'
import { findGridItem, findGridItemParentGrid } from './appUtils'
import ComponentNavigation from './component/ComponentNavigation.svelte'
import CssSettings from './componentsPanel/CssSettings.svelte'
@@ -130,12 +130,15 @@
previewTheme
})
let scale = writable(100)
setContext<AppEditorContext>('AppEditorContext', {
history,
pickVariableCallback,
movingcomponents: writable(undefined),
selectedComponentInEditor: writable(undefined),
jobsDrawerOpen: writable(false)
jobsDrawerOpen: writable(false),
scale
})
let timeout: NodeJS.Timeout | undefined = undefined
@@ -443,17 +446,48 @@
$focusedGrid = undefined
}}
class={twMerge(
'bg-surface h-full w-full relative',
'bg-surface-secondary h-full w-full relative',
$appStore.css?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<div class="absolute bottom-4 left-4 z-50">
<div class="flex flex-row gap-2 text-xs items-center">
<Button
color="light"
variant="border"
size="xs2"
disabled={$scale <= 30}
on:click={() => {
$scale -= 10
}}
>
<Minus size={14} />
</Button>
{$scale}
<Button
color="light"
variant="border"
size="xs2"
disabled={$scale >= 100}
on:click={() => {
$scale += 10
}}
>
<Plus size={14} />
</Button>
</div>
</div>
<div id="app-editor-top-level-drawer" />
<div
class="absolute inset-0 h-full w-full surface-secondary bg-[radial-gradient(#dbdbdb_1px,transparent_1px)] dark:bg-[radial-gradient(#666666_1px,transparent_1px)] [background-size:16px_16px]"
/>
<div
class={classNames(
'bg-surface-secondary/80 relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
'relative mx-auto w-full h-full overflow-auto',
$appStore.fullscreen ? '' : 'max-w-6xl border-x'
)}
>
{#if $appStore.grid}
@@ -485,7 +519,6 @@
size="xs"
class="h-full"
on:pointerdown={() => {
console.log('click', $cssEditorOpen)
if ($cssEditorOpen) {
$cssEditorOpen = false
selectedTab = 'insert'
@@ -119,7 +119,6 @@
appStore.subscribe(loadTheme)
async function loadTheme(currentAppStore: App) {
console.log(currentAppStore)
if (!currentAppStore.theme) {
return
}
@@ -190,9 +189,9 @@
<div
style={app.css?.['app']?.['grid']?.style}
class={twMerge(
'px-4 pt-4 pb-2 overflow-visible',
'p-2 overflow-visible',
app.css?.['app']?.['grid']?.class ?? '',
'wm-app-grid'
'wm-app-grid subgrid'
)}
bind:clientWidth={$parentWidth}
>
@@ -32,7 +32,7 @@
allIdsInPath
} = getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
const { history, scale } = getContext<AppEditorContext>('AppEditorContext')
let previousSelectedIds: string[] | undefined = undefined
$: if (!deepEqual(previousSelectedIds, $selectedComponent)) {
@@ -73,9 +73,9 @@
<div
style={$app.css?.['app']?.['grid']?.style}
class={twMerge(
'px-4 pt-4 pb-2 overflow-visible',
'p-2 overflow-visible',
$app.css?.['app']?.['grid']?.class ?? '',
'wm-app-grid'
'wm-app-grid border'
)}
on:pointerdown={() => {
$selectedComponent = undefined
@@ -83,7 +83,14 @@
}}
bind:clientWidth={$parentWidth}
>
<div class={!$focusedGrid && $mode !== 'preview' ? ' border border-dashed' : ''}>
<div
class={twMerge(
!$focusedGrid && $mode !== 'preview' ? 'border-dashed' : '',
'subgrid',
'border-[#999999] dark:border-[#aaaaaa] border '
)}
style={`transform: scale(${$scale / 100})`}
>
<Grid
allIdsInPath={$allIdsInPath}
selectedIds={$selectedComponent}
@@ -51,20 +51,21 @@
{#if $enterpriseLicense === undefined}
<div bind:clientHeight={alertHeight} class="p-2 flex flex-row gap-2">
<div class="flex flex-row items-center text-yellow-500 text-xs">
<div class="flex">
<div class="flex whitespace-nowrap">
<AlertTriangle size={16} />
EE only
</div>
<Tooltip>
<Tooltip light>
App CSS editor is an exclusive feature of the Enterprise Edition. You can
experiment with this feature in the editor, but please note that the changes
will not be visible once deployed.
</Tooltip>
</div>
<div class="flex flex-row items-center text-blue-500 text-xs">
Component styling is still available in the Community Edition
<Tooltip>
You can still style components individually in the Community Edition.
Component styling available in CE
<Tooltip light>
You can still style components in the Community Edition in the styling section
of the component's configuration.
</Tooltip>
</div>
</div>
@@ -72,7 +72,7 @@ export function getComponentControl(type: keyof typeof components): Array<Compon
case 'modalcomponent':
return [openModal, closeModal]
case 'aggridcomponent':
return [getAgGrid]
return [getAgGrid, setSelectedIndex]
case 'displaycomponent':
case 'dateinputcomponent':
case 'textinputcomponent':
@@ -90,7 +90,6 @@ export function getComponentControl(type: keyof typeof components): Array<Compon
case 'textareainputcomponent':
return [setValue]
case 'tablecomponent':
case 'aggridcomponent':
return [setSelectedIndex]
default:
if (components[type].initialData['componentInput']) {
@@ -27,7 +27,7 @@
)}
>
<div class="min-w-[150px]">
<div class="sticky z-10 top-0 left-0 w-full p-1.5">
<div class="sticky z-10 top-0 left-0 w-full p-1.5 bg-surface">
<ClearableInput bind:value={$search} placeholder="Search outputs..." />
</div>
@@ -6,7 +6,6 @@
import PanelSection from './common/PanelSection.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
export let groupFields: RichConfigurations | undefined
@@ -40,13 +39,13 @@
}
console.log(groupFields)
}}
options={{ right: 'container is a component group' }}
/>
<Tooltip
>Group fields allow inner components to depend on the group fields which make the container a
options={{
right: 'container is a component group',
rightTooltip: `Group fields allow inner components to depend on the group fields which make the container a
group of component that is encapsulated. Inside the group, it is possible to retrieve the values
using `group.<x />` where x is the group field name</Tooltip
>
using \`group.<x />\` where x is the group field name`
}}
/>
</div>
{#if groupFields != undefined}
<PanelSection
@@ -17,7 +17,7 @@
)}
>
<div class="flex justify-between flex-wrap items-center w-full gap-1">
<div class="text-sm inline-flex items-center font-semibold text-primary {titlePadding} gap-1">
<div class="text-xs inline-flex items-center font-semibold text-primary {titlePadding} gap-1">
<span class="truncate">
{title}
</span>
@@ -1,5 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
import type { AppEditorContext } from '../types'
import { writable } from 'svelte/store'
const dispatch = createEventDispatcher()
@@ -25,6 +27,9 @@
export let onTop
export let shadow: { x: number; y: number; w: number; h: number } | undefined = undefined
const ctx = getContext<AppEditorContext>('AppEditorContext')
const scale = ctx ? ctx.scale : writable(100)
const divId = `component-${id}`
let shadowElement
@@ -50,19 +55,14 @@
let irect = shadowElement.getBoundingClientRect()
let shadowBound
const factor = $scale / 100
if (subgrid) {
let subGridParent = subgrid.parentElement
let subGridParentRect = subGridParent.getBoundingClientRect()
let prect = subgrid.getBoundingClientRect()
const subGridOffset = {
x: subGridParentRect.x - prect.x,
y: subGridParentRect.y - prect.y
}
shadowBound = {
x: irect.x - prect.left - subGridOffset.x,
y: irect.y - prect.top - subGridOffset.y
x: irect.x - prect.left,
y: irect.y - prect.top
}
} else {
shadowBound = irect
@@ -71,8 +71,8 @@
const xdragBound = rect.left + cordDiff.x
const ydragBound = rect.top + cordDiff.y
cordDiff.x = shadow.x * xPerPx + gapX - (shadowBound.x - xdragBound)
cordDiff.y = shadow.y * yPerPx + gapY - (shadowBound.y - ydragBound)
cordDiff.x = shadow.x * xPerPx + gapX - (shadowBound.x - xdragBound * factor) * factor
cordDiff.y = shadow.y * yPerPx + gapY - (shadowBound.y - ydragBound * factor) * factor
active = false
trans = true
@@ -121,19 +121,13 @@
let irect = gridItem.getBoundingClientRect()
if (subgrid && subgrid.parentElement) {
let subGridParent = subgrid.parentElement
let subGridParentRect = subGridParent.getBoundingClientRect()
let prect = subgrid.getBoundingClientRect()
const subGridOffset = {
x: subGridParentRect.x - prect.x,
y: subGridParentRect.y - prect.y
}
const factor = $scale / 100
rect = {
top: irect.top - prect.top - subGridOffset.y,
left: irect.left - prect.left - subGridOffset.x
top: (irect.top - prect.top) / factor,
left: (irect.left - prect.left) / factor
}
} else {
rect = irect
@@ -145,8 +139,8 @@
dragClosure = () => {
dragClosure = undefined
initX = clientX
initY = clientY
initX = (clientX / $scale) * 100
initY = (clientY / $scale) * 100
dispatch('initmove')
}
window.addEventListener('pointermove', pointermove)
@@ -200,7 +194,7 @@
event.stopImmediatePropagation()
const { clientX, clientY } = event
const cordDiff = { x: clientX - initX, y: clientY - initY }
const cordDiff = { x: (clientX / $scale) * 100 - initX, y: (clientY / $scale) * 100 - initY }
dispatch('move', { cordDiff, clientY })
}
@@ -286,8 +280,8 @@
const resizePointerMove = ({ pageX, pageY }) => {
if (shadow) {
newSize.width = initSize.width + pageX - resizeInitPos.x
newSize.height = initSize.height + pageY - resizeInitPos.y
newSize.width = initSize.width + ((pageX - resizeInitPos.x) / $scale) * 100
newSize.height = initSize.height + ((pageY - resizeInitPos.y) / $scale) * 100
// Get max col number
let maxWidth = cols - shadow.x
@@ -297,8 +291,8 @@
newSize.width = Math.min(newSize.width, maxWidth * xPerPx - gapX * 2)
// Limit col & row
shadow.w = Math.round((newSize.width + gapX * 2) / xPerPx)
shadow.h = Math.round((newSize.height + gapY * 2) / yPerPx)
shadow.w = Math.round(Math.max((newSize.width + gapX * 2) / xPerPx, 1))
shadow.h = Math.round(Math.max((newSize.height + gapY * 2) / yPerPx, 1))
repaint(false, false)
}
@@ -314,6 +308,7 @@
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
draggable="false"
on:pointerdown|stopPropagation|preventDefault={pointerdown}
+2 -1
View File
@@ -249,7 +249,8 @@ export type AppEditorContext = {
pickVariableCallback: Writable<((path: string) => void) | undefined>
selectedComponentInEditor: Writable<string | undefined>
movingcomponents: Writable<string[] | undefined>
jobsDrawerOpen: Writable<boolean>
jobsDrawerOpen: Writable<boolean>,
scale: Writable<number>
}
export type FocusedGrid = { parentComponentId: string; subGridIndex: number }
@@ -37,8 +37,8 @@
}
}}
>
<ToggleButton value="trigger" label="Trigger" />
<ToggleButton value="sequence" label="Sequence" />
<ToggleButton value="trigger" label="Trigger" />
</ToggleButtonGroup>
{#each $modulesStore as copilotModule, i}
<div>
@@ -14,6 +14,7 @@
export let close: () => void
export let funcDesc: string
export let modules: FlowModule[]
export let trigger = false
// state
let input: HTMLInputElement | undefined
@@ -31,7 +32,8 @@
const ts = Date.now()
const scriptIds = await ScriptService.queryHubScripts({
text: `${text}`,
limit: 3
limit: 3,
kind: trigger ? 'trigger' : 'script'
})
if (ts < doneTs) return
doneTs = ts
@@ -60,7 +62,7 @@
$copilotModulesStore = [
{
id: nextId($flowStateStore, $flowStore),
type: 'script',
type: trigger ? 'trigger' : 'script',
description: funcDesc,
code: '',
source: selectedCompletion ? 'hub' : 'custom',
@@ -96,7 +98,7 @@
hubCompletions = []
}
}}
placeholder="AI Gen or search hub scripts"
placeholder="AI Gen or search hub {trigger ? 'triggers' : 'scripts'}"
/>
{#if funcDesc.length === 0}
<Icon
@@ -155,7 +157,7 @@
{/if}
{#if hubCompletions.length > 0}
<div class="text-left mt-2">
<p class="text-xs text-secondary ml-2">Hub Scripts</p>
<p class="text-xs text-secondary ml-2">Hub {trigger ? 'Triggers' : 'Scripts'}</p>
<ul class="transition-all divide-y">
{#each hubCompletions as item (item.path)}
<li>
+2 -2
View File
@@ -56,7 +56,7 @@ const additionalInfos: {
bun: `<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}_resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -65,7 +65,7 @@ Only define the type for resources that are actually needed to achieve the funct
</contextual_information>`,
python3: `<contextual_information>
We have to export a "main" function and specify the parameter types but do not call it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified (has to be IN LOWERCASE).
<resourceTypes>
{resourceTypes}
@@ -16,7 +16,7 @@ prompts:
```
<contextual_information>
We have to export a "main" function and specify the parameter types but do not call it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}_resource".
The resource type name has to be exactly as specified (has to be IN LOWERCASE).
<resourceTypes>
{resourceTypes}
@@ -33,7 +33,7 @@ prompts:
<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -133,7 +133,7 @@ prompts:
```
<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -150,7 +150,7 @@ prompts:
<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -2,10 +2,10 @@ export const EDIT_PROMPT = {
"system": "You write code as instructed by the user. Only output code. Wrap the code in a code block. \nPut explanations directly in the code as comments.\n\nHere's how interactions have to look like:\nuser: {sample_question}\nassistant: ```language\n{code}\n```",
"prompts": {
"python3": {
"prompt": "Here's my python3 code: \n```python\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified (has to be IN LOWERCASE).\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
"prompt": "Here's my python3 code: \n```python\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}_resource\". \nThe resource type name has to be exactly as specified (has to be IN LOWERCASE).\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
},
"deno": {
"prompt": "Here's my TypeScript code in a deno running environment:\n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
"prompt": "Here's my TypeScript code in a deno running environment:\n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\". \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
},
"go": {
"prompt": "Here's my go code: \n```go\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function. Import the packages you need. The return type of the function has to be ({return_type}, error). The file package has to be \"inner\"\n</contextual_information>\nMy instructions: {description}"
@@ -32,10 +32,10 @@ export const EDIT_PROMPT = {
"prompt": "Here's my powershell code: \n```powershell\n{code}\n```\n<contextual_information>\nArguments can be obtained by calling the param function on the first line like that: `param($ParamName1, $ParamName2 = \"default value\", [{type}]$ParamName3, ...)`\nI get the following error: {error}\n</contextual_information>\nMy instructions: {description}"
},
"nativets": {
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\". \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
},
"bun": {
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\". \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nMy instructions: {description}"
},
"frontend": {
"prompt": "Here's my client-side javascript code: \n```javascript\n{code}\n```\n<contextual_information>\nYou can access the context object with the ctx global variable. \nThe app state is a store that can be used to store data. You can access and update the state object with the state global variable like this: state.foo = 'bar'\nYou can use the goto function to navigate to a specific URL: goto(path: string, newTab?: boolean)\nUse the setTab function to manually set the tab of a Tab component: setTab(id: string, index: string)\nUse the recompute function to recompute a component: recompute(id: string)\nUse the getAgGrid function to get the ag-grid instance of a table: getAgGrid(id: string)\nThe setValue function is meant to set or force the value of a component: setValue(id: string, value: any).\n</contextual_information>\nMy instructions: {description}"
@@ -18,7 +18,7 @@ prompts:
```
<contextual_information>
We have to export a "main" function and specify the parameter types but do not call it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}_resource".
The resource type name has to be exactly as specified (has to be IN LOWERCASE).
<resourceTypes>
{resourceTypes}
@@ -36,7 +36,7 @@ prompts:
<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -141,7 +141,7 @@ prompts:
```
<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -159,7 +159,7 @@ prompts:
<contextual_information>
We have to export a "main" function like this: "export async function main(...)" and specify the parameter types but do not call it.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -2,10 +2,10 @@ export const FIX_PROMPT = {
"system": "You fix the code shared by the user. Only output code. Wrap the code in a code block. \nExplain the error and the fix after generating the code inside an <explanation> tag.\nAlso put explanations directly in the code as comments.\n\nHere's how interactions have to look like:\nuser: {sample_question}\nassistant: ```language\n{code}\n```\n<explanation>{explanation}</explanation>",
"prompts": {
"python3": {
"prompt": "Here's my python3 code: \n```python\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified (has to be IN LOWERCASE).\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
"prompt": "Here's my python3 code: \n```python\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}_resource\". \nThe resource type name has to be exactly as specified (has to be IN LOWERCASE).\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
},
"deno": {
"prompt": "Here's my TypeScript code in a deno running environment:\n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
"prompt": "Here's my TypeScript code in a deno running environment:\n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\". \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
},
"go": {
"prompt": "Here's my go code: \n```go\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function. Import the packages you need. The return type of the function has to be ({return_type}, error). The file package has to be \"inner\"\n</contextual_information>\nI get the following error: {error}\nFix my code."
@@ -32,10 +32,10 @@ export const FIX_PROMPT = {
"prompt": "Here's my powershell code: \n```powershell\n{code}\n```\n<contextual_information>\nArguments can be obtained by calling the param function on the first line like that: `param($ParamName1, $ParamName2 = \"default value\", [{type}]$ParamName3, ...)`\n</contextual_information>\nI get the following error: {error}\nFix my code."
},
"nativets": {
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\". \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
},
"bun": {
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
"prompt": "Here's my TypeScript code: \n```typescript\n{code}\n```\n<contextual_information>\nWe have to export a \"main\" function like this: \"export async function main(...)\" and specify the parameter types but do not call it.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\". \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE.\n</contextual_information>\nI get the following error: {error}\nFix my code."
}
}
};
@@ -11,7 +11,7 @@ prompts:
python3:
prompt: |-
Write a function in python called "main". The function should {description}. Specify the parameter types. Do not call the main function.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}_resource".
The resource type name has to be exactly as specified (has to be IN LOWERCASE).
<resourceTypes>
{resourceTypes}
@@ -21,7 +21,7 @@ prompts:
prompt: |-
Write a function in TypeScript called "main". The function should {description}. Specify the parameter types. You are in a Deno environment. You can import deno libraries or you can also import npm libraries like that: "import ... from "npm:{package}";". Export the "main" function like this: "export async function main(...)". Do not call the main function.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -54,7 +54,7 @@ prompts:
nativets:
prompt: |-
Write a function in TypeScript called "main". The function should {description}. Specify the parameter types. You should use fetch and are not allowed to import any libraries. Export the "main" function like this: "export async function main(...)". Do not call the main function.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -64,7 +64,7 @@ prompts:
prompt: |-
Write a function in TypeScript called "main". The function should {description}. Specify the parameter types. You can import npm libraries. Export the "main" function like this: "export async function main(...)". Do not call the main function.
If needed, the standard fetch method is available globally, do not import it.
You can take as parameters resources which are dictionaries containing credentials or configuration information.
You can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: "{resource_type}Resource".
The resource type name has to be exactly as specified.
<resourceTypes>
{resourceTypes}
@@ -2,10 +2,10 @@ export const GEN_PROMPT = {
"system": "You write code as instructed by the user. Only output code. Wrap the code in a code block. \nPut explanations directly in the code as comments.\n\nHere's how interactions have to look like:\nuser: {sample_question}\nassistant: ```language\n{code}\n```",
"prompts": {
"python3": {
"prompt": "Write a function in python called \"main\". The function should {description}. Specify the parameter types. Do not call the main function.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified (has to be IN LOWERCASE).\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
"prompt": "Write a function in python called \"main\". The function should {description}. Specify the parameter types. Do not call the main function.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}_resource\".\nThe resource type name has to be exactly as specified (has to be IN LOWERCASE).\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
},
"deno": {
"prompt": "Write a function in TypeScript called \"main\". The function should {description}. Specify the parameter types. You are in a Deno environment. You can import deno libraries or you can also import npm libraries like that: \"import ... from \"npm:{package}\";\". Export the \"main\" function like this: \"export async function main(...)\". Do not call the main function.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
"prompt": "Write a function in TypeScript called \"main\". The function should {description}. Specify the parameter types. You are in a Deno environment. You can import deno libraries or you can also import npm libraries like that: \"import ... from \"npm:{package}\";\". Export the \"main\" function like this: \"export async function main(...)\". Do not call the main function.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\".\nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
},
"go": {
"prompt": "Write a function in go called \"main\". The function should {description}. Import the packages you need. The return type of the function has to be ({return_type}, error). The file package has to be \"inner\"."
@@ -32,10 +32,10 @@ export const GEN_PROMPT = {
"prompt": "Write powershell code that should {description}. Arguments can be obtained by calling the param function on the first line like that: `param($ParamName1, $ParamName2 = \"default value\", [{type}]$ParamName3, ...)`"
},
"nativets": {
"prompt": "Write a function in TypeScript called \"main\". The function should {description}. Specify the parameter types. You should use fetch and are not allowed to import any libraries. Export the \"main\" function like this: \"export async function main(...)\". Do not call the main function.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information.\nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
"prompt": "Write a function in TypeScript called \"main\". The function should {description}. Specify the parameter types. You should use fetch and are not allowed to import any libraries. Export the \"main\" function like this: \"export async function main(...)\". Do not call the main function.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\".\nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
},
"bun": {
"prompt": "Write a function in TypeScript called \"main\". The function should {description}. Specify the parameter types. You can import npm libraries. Export the \"main\" function like this: \"export async function main(...)\". Do not call the main function.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. \nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
"prompt": "Write a function in TypeScript called \"main\". The function should {description}. Specify the parameter types. You can import npm libraries. Export the \"main\" function like this: \"export async function main(...)\". Do not call the main function.\nIf needed, the standard fetch method is available globally, do not import it.\nYou can take as parameters resources which are dictionaries containing credentials or configuration information. Name the resource parameters like this: \"{resource_type}Resource\".\nThe resource type name has to be exactly as specified.\n<resourceTypes>\n{resourceTypes}\n</resourceTypes>\nOnly define the type for resources that are actually needed to achieve the function purpose. If the type name conflicts with the imported object, rename the imported object NOT THE TYPE."
},
"frontend": {
"prompt": "Write client-side javascript code that should {description}. You have access to a few helpers:\nYou can access the context object with the ctx global variable. \nThe app state is a store that can be used to store data. You can access and update the state object with the state global variable like this: state.foo = 'bar'\nYou can use the goto function to navigate to a specific URL: goto(path: string, newTab?: boolean)\nUse the setTab function to manually set the tab of a Tab component: setTab(id: string, index: string)\nUse the recompute function to recompute a component: recompute(id: string)\nUse the getAgGrid function to get the ag-grid instance of a table: getAgGrid(id: string)\nThe setValue function is meant to set or force the value of a component: setValue(id: string, value: any)."
@@ -15,7 +15,7 @@
$: scriptModules = dfs($flowStore.value.modules, (x) => x)
.map((x) => [x.value, x] as [FlowModuleValue, FlowModule])
.filter((x) => x[0].type == 'script' || x[0].type == 'rawscript') as [
.filter((x) => x[0].type == 'script' || x[0].type == 'rawscript' || x[0].type == 'flow') as [
PathScript | RawScript,
FlowModule
][]
@@ -96,7 +96,7 @@
<span class="text-xs text-tertiary">delay = multiplier * base ^ (number of attempt)</span>
<input bind:value={flowModule.retry.exponential.multiplier} type="number" />
<div class="text-xs font-bold !mt-2">Base (in seconds)</div>
<input bind:value={flowModule.retry.exponential.seconds} type="number" />
<input bind:value={flowModule.retry.exponential.seconds} type="number" step="1" />
{:else}
<div class="text-xs font-bold !mt-2">Attempts</div>
<input type="number" disabled />
@@ -0,0 +1,51 @@
<script lang="ts">
import { Menu } from '$lib/components/common'
import { faBolt } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import StepGen from '$lib/components/copilot/StepGen.svelte'
import type { FlowModule } from '$lib/gen'
const dispatch = createEventDispatcher()
export let open: boolean | undefined = undefined
export let index: number
export let funcDesc = ''
export let modules: FlowModule[]
$: !open && (funcDesc = '')
</script>
<Menu
transitionDuration={0}
pointerDown
bind:show={open}
noMinW
placement="bottom-center"
let:close
>
<button
title="Add a Trigger"
slot="trigger"
type="button"
class="text-primary bg-surface border mx-0.5 rotate-180 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
>
<Icon data={faBolt} scale={0.8} />
</button>
<StepGen {index} bind:funcDesc bind:open {close} {modules} trigger />
{#if funcDesc.length === 0}
<div class="font-mono divide-y text-xs w-full text-secondary">
<button
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
on:pointerdown={() => {
close()
dispatch('new', 'trigger')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faBolt} scale={0.8} class="mr-2" />
Trigger
</button>
</div>
{/if}
</Menu>
@@ -2,7 +2,7 @@
import { Badge } from '$lib/components/common'
import type { FlowModule } from '$lib/gen'
import { classNames } from '$lib/utils'
import { faBolt, faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
import { faMagicWandSparkles } from '@fortawesome/free-solid-svg-icons'
import { ClipboardCopy, ExternalLink, X } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
@@ -10,6 +10,7 @@
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import { existsOpenaiResourcePath } from '$lib/stores'
import Menu from '$lib/components/common/menu/Menu.svelte'
import InsertTriggerButton from './InsertTriggerButton.svelte'
export let label: string
export let modules: FlowModule[] | undefined
@@ -34,6 +35,7 @@
deleteBranch: { module: FlowModule; index: number }
}>()
let openMenu = false
let triggerOpenMenu = false
let openNoCopilot = false
const { drawerStore: copilotDrawerStore, currentStepStore: copilotCurrentStepStore } =
@@ -57,6 +59,7 @@
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'w-full flex relative overflow-hidden rounded-sm',
@@ -177,18 +180,24 @@
{/if}
</Menu>
</div>
<div class="w-7 absolute top-12 left-[65%] right-[35%] -translate-x-1/2">
<button
title="Add a Trigger"
on:click={() => {
<div
class="{triggerOpenMenu
? 'z-10'
: ''} w-7 absolute top-12 left-[65%] right-[35%] -translate-x-1/2"
>
<InsertTriggerButton
bind:open={triggerOpenMenu}
on:new={(e) => {
if (modules) {
dispatch('insert', { modules, index: 0, detail: 'trigger' })
dispatch('insert', {
modules,
index: 0,
detail: e.detail
})
}
}}
type="button"
class="text-primary bg-surface border mx-0.5 rotate-180 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
>
<Icon data={faBolt} scale={0.8} />
</button>
index={0}
modules={modules ?? []}
/>
</div>
{/if}
@@ -95,7 +95,7 @@
{#if filter.length > 0 && filteredItems.length == 0}
<NoItemFound />
{/if}
<ul class="divide-y border rounded-md overflow-hidden">
<ul class="divide-y border rounded-md">
{#each filteredItems as { path, hash, summary, description, marked }}
<li class="flex flex-row w-full">
<button
@@ -14,6 +14,7 @@ export interface Setting {
| 'textarea'
| 'seconds'
| 'email'
| 'license_key'
storage: SettingStorage
}
@@ -0,0 +1,18 @@
import { ResourceService } from "$lib/gen";
import { workspaceStore } from "$lib/stores";
import { get, writable } from "svelte/store";
export const resourceTypesStore = writable<string[] | undefined>(undefined)
export async function getResourceTypes() {
const rts = get(resourceTypesStore)
if (rts) {
return rts
} else {
const nrts = await ResourceService.listResourceTypeNames({ workspace: get(workspaceStore)! })
resourceTypesStore.set(nrts)
return nrts
}
}
@@ -10,7 +10,7 @@
// Filters
export let path: string | null = null
export let success: boolean | undefined = undefined
export let success: "running" | "success" | "failure" | undefined = undefined
export let isSkipped: boolean | undefined = undefined
export let argFilter: string
export let argError: string
@@ -187,8 +187,9 @@
<span class="text-xs absolute -top-4">Status</span>
<ToggleButtonGroup bind:selected={success}>
<ToggleButton value={undefined} label="All" />
<ToggleButton value={true} label="Success" class="whitespace-nowrap" />
<ToggleButton value={false} label="Failure" class="whitespace-nowrap" />
<ToggleButton value={"running"} label="Running" class="whitespace-nowrap" />
<ToggleButton value={"success"} label="Success" class="whitespace-nowrap" />
<ToggleButton value={"failure"} label="Failure" class="whitespace-nowrap" />
</ToggleButtonGroup>
</div>
<div class="relative w-32">
@@ -90,7 +90,7 @@
switchWorkspace('admins')
close()
}}
class="text-secondary block px-4 py-2 font-normal text-sm hover:bg-surface-hover hover:text-primary"
class="text-secondary block text-left px-4 py-2 font-normal text-sm hover:bg-surface-hover hover:text-primary w-full"
role="menuitem"
tabindex="-1"
>
@@ -15,6 +15,7 @@
import Popover from '$lib/components/Popover.svelte'
import Required from '$lib/components/Required.svelte'
import ResourceEditor from '$lib/components/ResourceEditor.svelte'
import { resourceTypesStore } from '$lib/components/resourceTypesStore'
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
import SearchItems from '$lib/components/SearchItems.svelte'
@@ -175,6 +176,7 @@
resourceTypeDrawer.closeDrawer?.()
sendUserToast('Resource type created')
loadResourceTypes()
$resourceTypesStore = undefined
}
async function updateResourceType(): Promise<void> {
@@ -42,10 +42,7 @@
let user: string | null = $page.url.searchParams.get('user')
let folder: string | null = $page.url.searchParams.get('folder')
// Rest of filters handled by RunsFilter
let success: boolean | undefined =
$page.url.searchParams.get('success') != undefined
? $page.url.searchParams.get('success') == 'true'
: undefined
let success: "running" | "success" | "failure" | undefined = ($page.url.searchParams.get('success') ?? undefined) as "running" | "success" | "failure" | undefined
let isSkipped: boolean | undefined =
$page.url.searchParams.get('is_skipped') != undefined
? $page.url.searchParams.get('is_skipped') == 'true'
@@ -128,7 +125,8 @@
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
jobKinds,
success,
success: success == "success" ? true : (success == 'failure' ? false : undefined),
running: success == 'running' ? true : undefined,
isSkipped,
isFlowStep: jobKindsCat != 'all' ? false : undefined,
args:
@@ -166,34 +164,38 @@
async function syncer() {
getCount()
if (sync && jobs && maxTs == undefined) {
let ts: string | undefined = undefined
let cursor = 0
while (cursor < jobs.length && minTs == undefined) {
let invCursor = jobs.length - 1 - cursor
let isQueuedJob = cursor == jobs?.length - 1 || jobs[invCursor].type == Job.type.QUEUED_JOB
if (isQueuedJob) {
if (cursor > 0) {
const date = new Date(jobs[invCursor + 1]?.created_at!)
date.setMilliseconds(date.getMilliseconds() + 1)
ts = date.toISOString()
if (success == 'running') {
loadJobs()
} else {
let ts: string | undefined = undefined
let cursor = 0
while (cursor < jobs.length && minTs == undefined) {
let invCursor = jobs.length - 1 - cursor
let isQueuedJob = cursor == jobs?.length - 1 || jobs[invCursor].type == Job.type.QUEUED_JOB
if (isQueuedJob) {
if (cursor > 0) {
const date = new Date(jobs[invCursor + 1]?.created_at!)
date.setMilliseconds(date.getMilliseconds() + 1)
ts = date.toISOString()
}
break
}
break
cursor++
}
cursor++
}
loading = true
const newJobs = await fetchJobs(maxTs, minTs ?? ts)
if (newJobs && newJobs.length > 0 && jobs) {
const oldJobs = jobs?.map((x) => x.id)
jobs = newJobs.filter((x) => !oldJobs.includes(x.id)).concat(jobs)
newJobs
.filter((x) => oldJobs.includes(x.id))
.forEach((x) => (jobs![jobs?.findIndex((y) => y.id == x.id)!] = x))
jobs = jobs
computeCompletedJobs()
loading = true
const newJobs = await fetchJobs(maxTs, minTs ?? ts)
if (newJobs && newJobs.length > 0 && jobs) {
const oldJobs = jobs?.map((x) => x.id)
jobs = newJobs.filter((x) => !oldJobs.includes(x.id)).concat(jobs)
newJobs
.filter((x) => oldJobs.includes(x.id))
.forEach((x) => (jobs![jobs?.findIndex((y) => y.id == x.id)!] = x))
jobs = jobs
computeCompletedJobs()
}
loading = false
}
loading = false
}
}
@@ -204,10 +206,7 @@
path = $page.params.path
user = $page.url.searchParams.get('user')
folder = $page.url.searchParams.get('folder')
success =
$page.url.searchParams.get('success') != undefined
? $page.url.searchParams.get('success') == 'true'
: undefined
success = ($page.url.searchParams.get('success') ?? undefined) as "success" | "failure" | "running" | undefined
isSkipped =
$page.url.searchParams.get('is_skipped') != undefined
? $page.url.searchParams.get('is_skipped') == 'true'
+7
View File
@@ -18,6 +18,13 @@
</script>
<svelte:head>
{#if !import.meta.env.PROD}
<script
data-project-id="JLtc2teApu4xqh47avxGx7oTAwY0rqAdVTqsV8MC"
data-is-production-environment="false"
src="https://snippet.meticulous.ai/v1/meticulous.js"
></script>
{/if}
<title>{$page.data?.stuff?.title ? `${$page.data?.stuff?.title} | ` : ''}Windmill</title>
</svelte:head>
+1 -4
View File
@@ -583,7 +583,6 @@ const config = {
fontSize: '18px',
fontWeight: theme('fontWeight.semibold'),
lineHeight: '1.3',
color: theme('colors.gray.600'),
[`@media (min-width: ${theme('screens.qhd')})`]: {
fontSize: '20px'
}
@@ -592,7 +591,6 @@ const config = {
fontSize: '16px',
fontWeight: theme('fontWeight.semibold'),
lineHeight: '1.5',
color: theme('colors.gray.600'),
[`@media (min-width: ${theme('screens.qhd')})`]: {
fontSize: '18px'
}
@@ -601,7 +599,6 @@ const config = {
fontSize: '16px',
fontWeight: theme('fontWeight.medium'),
lineHeight: '1.5',
color: theme('colors.gray.600'),
[`@media (min-width: ${theme('screens.qhd')})`]: {
fontSize: '18px'
}
@@ -789,7 +786,7 @@ const config = {
overflow: 'auto !important'
},
'.splitpanes__splitter': {
backgroundColor: lightTheme.border + ' !important',
backgroundColor: 'rgb(243 244 246) !important',
margin: '0 !important',
border: 'none !important',
'&::after': {
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.175.0"
wmill_pg = ">=1.175.0"
wmill = ">=1.176.0"
wmill_pg = ">=1.176.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.175.0
version: 1.176.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.175.0"
version = "1.176.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
@@ -16,7 +16,7 @@ include = ["wmill/py.typed"]
[tool.poetry.dependencies]
python = "^3.7"
windmill-api = "^1.175.0"
windmill-api = "^1.176.0"
[build-system]
requires = ["poetry>=1.0.2", "poetry-dynamic-versioning"]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.175.0"
version = "1.176.0"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.175.0",
"version": "1.176.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.175.0
1.176.0