Compare commits

..

2 Commits

Author SHA1 Message Date
Ruben Fiszel
4fceae4f85 progress 2023-02-01 20:54:13 +01:00
Ruben Fiszel
c1cfed840a all 2023-02-01 16:27:50 +01:00
95 changed files with 2376 additions and 4327 deletions

View File

@@ -1,26 +1,6 @@
# Changelog
## [1.62.0](https://github.com/windmill-labs/windmill/compare/v1.61.1...v1.62.0) (2023-02-03)
### Features
* add INCLUDE_HEADERS env variable to pass value from request headers ([0921ba0](https://github.com/windmill-labs/windmill/commit/0921ba008535e945f2ec3255728c2e8c1f4c36dc))
* add WHITELIST_WORKSPACES and BLACKLIST_WORKSPACES ([99568ea](https://github.com/windmill-labs/windmill/commit/99568eaa473d57123a7dde4007f8812e0053fb3f))
* Add workspace webhook ([#1158](https://github.com/windmill-labs/windmill/issues/1158)) ([b9ac60f](https://github.com/windmill-labs/windmill/commit/b9ac60f8bb0662e364606c4b7b8a6e3c1e7e4041))
* adding worker_busy ([23007f7](https://github.com/windmill-labs/windmill/commit/23007f7a71630fc2040e1be39db83ba56689e3c4))
* **cli:** 2-Way sync ([#1071](https://github.com/windmill-labs/windmill/issues/1071)) ([cdd1619](https://github.com/windmill-labs/windmill/commit/cdd16195aeaf32e1f1d0648f48e4843954d16d9c))
* **frontend:** App initial loading animations ([#1176](https://github.com/windmill-labs/windmill/issues/1176)) ([3305481](https://github.com/windmill-labs/windmill/commit/3305481d5d4ce598ceb57256cea851869cdaf25e))
* **python:** add ADDITIONAL_PYTHON_PATHS ([14b32be](https://github.com/windmill-labs/windmill/commit/14b32be8b229372c57a167fd74cb958a96f0e8e6))
### Bug Fixes
* **frontend:** Render popups above components in app editor ([#1171](https://github.com/windmill-labs/windmill/issues/1171)) ([bc8d1a3](https://github.com/windmill-labs/windmill/commit/bc8d1a375ec7886357ce0ef5971bb35013c94d61))
* **frontend:** Various fixes and improvements ([#1177](https://github.com/windmill-labs/windmill/issues/1177)) ([9f5500c](https://github.com/windmill-labs/windmill/commit/9f5500c1965ea50796d3bf289c0f9e0c929427f4))
* navigate to new script page before saving script ([f171cd8](https://github.com/windmill-labs/windmill/commit/f171cd8b7c46677173572bac256cbb489a1b8526))
## [1.61.1](https://github.com/windmill-labs/windmill/compare/v1.61.0...v1.61.1) (2023-01-31)

View File

@@ -278,8 +278,7 @@ upcoming CLI tool.
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker) | Worker |
| METRICS_ADDR | None | The socket addr at which to expose Prometheus metrics at the /metrics path. Set to "true" to expose it on port 8001 | All |
| JSON_FMT | false | Output the logs in json format instead of logfmt | All |
| BASE_URL | http://localhost:8000 | The base url that is exposed publicly to access your instance | Server |
@@ -312,8 +311,6 @@ upcoming CLI tool.
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
| WHITELIST_WORKSPACES | None | Whitelist of workspaces this worker takes job from | Worker |
| BLACKLIST_WORKSPACES | None | Blacklist of workspaces this worker takes job from | Worker |

185
backend/Cargo.lock generated
View File

@@ -408,9 +408,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.4.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]]
name = "cargo-lock"
@@ -661,9 +661,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.89"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9"
checksum = "322296e2f2e5af4270b54df9e85a02ff037e271af20ba3e7fe1575515dc840b8"
dependencies = [
"cc",
"cxxbridge-flags",
@@ -673,9 +673,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.89"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d"
checksum = "017a1385b05d631e7875b1f151c9f012d37b53491e2a87f65bff5c262b2111d8"
dependencies = [
"cc",
"codespan-reporting",
@@ -688,15 +688,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.89"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a"
checksum = "c26bbb078acf09bc1ecda02d4223f03bdd28bd4874edcb0379138efc499ce971"
[[package]]
name = "cxxbridge-macro"
version = "1.0.89"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2"
checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e"
dependencies = [
"proc-macro2",
"quote",
@@ -928,9 +928,9 @@ dependencies = [
[[package]]
name = "encoding_rs"
version = "0.8.32"
version = "0.8.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
dependencies = [
"cfg-if",
]
@@ -992,7 +992,7 @@ dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@@ -1308,9 +1308,9 @@ dependencies = [
[[package]]
name = "heck"
version = "0.4.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
dependencies = [
"unicode-segmentation",
]
@@ -1429,9 +1429,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.24"
version = "0.14.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
dependencies = [
"bytes",
"futures-channel",
@@ -1538,12 +1538,12 @@ dependencies = [
[[package]]
name = "io-lifetimes"
version = "1.0.5"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
dependencies = [
"libc",
"windows-sys 0.45.0",
"windows-sys",
]
[[package]]
@@ -1574,7 +1574,7 @@ dependencies = [
"hermit-abi 0.2.6",
"io-lifetimes",
"rustix",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@@ -1603,9 +1603,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.61"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
dependencies = [
"wasm-bindgen",
]
@@ -1907,7 +1907,7 @@ dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@@ -2168,7 +2168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core 0.9.7",
"parking_lot_core 0.9.6",
]
[[package]]
@@ -2187,15 +2187,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.7"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-sys 0.45.0",
"windows-sys",
]
[[package]]
@@ -2462,7 +2462,7 @@ dependencies = [
[[package]]
name = "progenitor"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
dependencies = [
"anyhow",
"built",
@@ -2480,7 +2480,7 @@ dependencies = [
[[package]]
name = "progenitor-client"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
dependencies = [
"bytes",
"futures-core",
@@ -2494,7 +2494,7 @@ dependencies = [
[[package]]
name = "progenitor-impl"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
dependencies = [
"getopts",
"heck",
@@ -2516,7 +2516,7 @@ dependencies = [
[[package]]
name = "progenitor-macro"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
dependencies = [
"openapiv3",
"proc-macro2",
@@ -2863,7 +2863,7 @@ dependencies = [
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@@ -2890,7 +2890,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython#b6e447145800ec7888f035bf266329e4b457e5f9"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -2899,7 +2899,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython#b6e447145800ec7888f035bf266329e4b457e5f9"
dependencies = [
"bincode",
"bitflags",
@@ -2916,7 +2916,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#adc23253e4b58980b407ba2760dbe61681d752fc"
source = "git+https://github.com/RustPython/RustPython#b6e447145800ec7888f035bf266329e4b457e5f9"
dependencies = [
"ahash",
"anyhow",
@@ -2965,7 +2965,7 @@ version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@@ -2979,7 +2979,7 @@ dependencies = [
"schemars_derive",
"serde",
"serde_json",
"uuid 1.3.0",
"uuid 1.2.2",
]
[[package]]
@@ -3458,7 +3458,7 @@ dependencies = [
"thiserror",
"tokio-stream",
"url",
"uuid 1.3.0",
"uuid 1.2.2",
"webpki-roots",
"whoami",
]
@@ -3707,9 +3707,9 @@ dependencies = [
[[package]]
name = "sync_wrapper"
version = "0.1.2"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
[[package]]
name = "tempfile"
@@ -3843,9 +3843,9 @@ dependencies = [
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
@@ -3865,7 +3865,7 @@ dependencies = [
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.42.0",
"windows-sys",
]
[[package]]
@@ -4172,7 +4172,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typify"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#77a43da6b1e9a79fe3ec719a038f4ea949c0342e"
source = "git+https://github.com/oxidecomputer/typify#cd1f95b66b0d0fbb33537d66043f26512ee3268b"
dependencies = [
"typify-impl",
"typify-macro",
@@ -4181,7 +4181,7 @@ dependencies = [
[[package]]
name = "typify-impl"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#77a43da6b1e9a79fe3ec719a038f4ea949c0342e"
source = "git+https://github.com/oxidecomputer/typify#cd1f95b66b0d0fbb33537d66043f26512ee3268b"
dependencies = [
"heck",
"log",
@@ -4199,7 +4199,7 @@ dependencies = [
[[package]]
name = "typify-macro"
version = "0.0.11-dev"
source = "git+https://github.com/oxidecomputer/typify#77a43da6b1e9a79fe3ec719a038f4ea949c0342e"
source = "git+https://github.com/oxidecomputer/typify#cd1f95b66b0d0fbb33537d66043f26512ee3268b"
dependencies = [
"proc-macro2",
"quote",
@@ -4224,7 +4224,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd"
dependencies = [
"rand 0.8.5",
"uuid 1.3.0",
"uuid 1.2.2",
]
[[package]]
@@ -4392,9 +4392,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.3.0"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [
"getrandom 0.2.8",
"serde",
@@ -4478,9 +4478,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.84"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -4488,9 +4488,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.84"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
dependencies = [
"bumpalo",
"log",
@@ -4503,9 +4503,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.34"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if",
"js-sys",
@@ -4515,9 +4515,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.84"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4525,9 +4525,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.84"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
"proc-macro2",
"quote",
@@ -4538,9 +4538,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.84"
version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "wasm-streams"
@@ -4557,9 +4557,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.61"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -4638,7 +4638,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"axum",
@@ -4665,7 +4665,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"argon2",
@@ -4685,7 +4685,6 @@ dependencies = [
"lazy_static",
"magic-crypt",
"mime_guess",
"prometheus",
"rand 0.8.5",
"reqwest",
"retainer",
@@ -4719,7 +4718,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"base64 0.21.0",
"chrono",
@@ -4729,12 +4728,12 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"uuid 1.3.0",
"uuid 1.2.2",
]
[[package]]
name = "windmill-audit"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"chrono",
"serde",
@@ -4747,7 +4746,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"axum",
@@ -4766,12 +4765,12 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"uuid 1.3.0",
"uuid 1.2.2",
]
[[package]]
name = "windmill-parser"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"serde",
"serde_json",
@@ -4779,7 +4778,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"itertools",
@@ -4793,7 +4792,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"itertools",
@@ -4805,7 +4804,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"itertools",
@@ -4820,7 +4819,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"deno_core",
@@ -4834,7 +4833,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"chrono",
@@ -4850,14 +4849,14 @@ dependencies = [
"sqlx",
"tracing",
"ulid",
"uuid 1.3.0",
"uuid 1.2.2",
"windmill-audit",
"windmill-common",
]
[[package]]
name = "windmill-worker"
version = "1.62.0"
version = "1.61.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -4878,7 +4877,7 @@ dependencies = [
"sqlx",
"tokio",
"tracing",
"uuid 1.3.0",
"uuid 1.2.2",
"windmill-api-client",
"windmill-audit",
"windmill-common",
@@ -4905,30 +4904,6 @@ dependencies = [
"windows_x86_64_msvc",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.1"

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.62.0"
version = "1.61.1"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.62.0"
version = "1.61.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1 +0,0 @@
-- Add down migration script here

View File

@@ -1,5 +0,0 @@
-- Add up migration script here
ALTER TABLE
workspace_settings
ADD
COLUMN webhook text;

View File

@@ -558,11 +558,6 @@
"name": "plan",
"ordinal": 8,
"type_info": "Varchar"
},
{
"name": "webhook",
"ordinal": 9,
"type_info": "Text"
}
],
"nullable": [
@@ -574,7 +569,6 @@
true,
true,
true,
true,
true
],
"parameters": {
@@ -1237,18 +1231,6 @@
},
"query": "INSERT INTO usr\n (workspace_id, email, username, is_admin, operator)\n VALUES ($1, $2, $3, $4, $5)"
},
"33d69b3915ddfde40323ace65c14e39fa4bbc8b5dd50a34e165765eaea1f4966": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1"
},
"355dcb2cbebd13f0e3bdd4929b9e431b0e6d72716d1c4f9ab6af6adce5b5e4b3": {
"describe": {
"columns": [
@@ -1868,11 +1850,6 @@
"name": "plan",
"ordinal": 8,
"type_info": "Varchar"
},
{
"name": "webhook",
"ordinal": 9,
"type_info": "Text"
}
],
"nullable": [
@@ -1884,7 +1861,6 @@
true,
true,
true,
true,
true
],
"parameters": {
@@ -2734,19 +2710,6 @@
},
"query": "UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash"
},
"8292b7b2cce5319575bc09ad18f29b63270872b6e5c6df1f0a326370058f13b0": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2"
},
"82f3c4cd1c1f6aea86d66f675442587684391bc32be9ab55ae20aab549b7bba5": {
"describe": {
"columns": [],
@@ -3802,26 +3765,6 @@
},
"query": "SELECT email FROM usr where username = $1 AND workspace_id = $2"
},
"a34b79872766941cae2d62c99d80e28b7214dd2fcbb68020a63325bbcb34f417": {
"describe": {
"columns": [
{
"name": "webhook",
"ordinal": 0,
"type_info": "Text"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT webhook FROM workspace_settings WHERE workspace_id = $1"
},
"a38059dc3574da498ce986c916b6d385b1f18d5bd659ef13c43fafa9daff6bda": {
"describe": {
"columns": [

View File

@@ -70,5 +70,4 @@ cookie.workspace = true
sha2.workspace = true
urlencoding.workspace = true
async-stripe.workspace = true
lazy_static.workspace = true
prometheus.workspace = true
lazy_static.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.62.0
version: 1.61.1
title: Windmill API
contact:
@@ -883,8 +883,6 @@ paths:
type: string
customer_id:
type: string
webhook:
type: string
/w/{workspace}/workspaces/premium_info:
get:
@@ -964,33 +962,6 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/edit_webhook:
post:
summary: edit webhook
operationId: editWebhook
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: WorkspaceWebhook
required: true
content:
application/json:
schema:
type: object
properties:
webhook:
type: string
responses:
"200":
description: status
content:
text/plain:
schema:
type: string
/w/{workspace}/users/list:
get:
summary: list users

View File

@@ -12,7 +12,6 @@ use crate::{
jobs::script_path_to_payload,
users::{require_owner_of_path, Authed, OptAuthed},
variables::build_crypt,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Json, Path, Query},
@@ -311,7 +310,6 @@ async fn get_secret_id(
async fn create_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(app): Json<CreateApp>,
) -> Result<(StatusCode, String)> {
@@ -358,12 +356,7 @@ async fn create_app(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() },
);
Ok((StatusCode::CREATED, app.path))
}
@@ -402,7 +395,6 @@ pub async fn get_hub_app_by_id(
async fn delete_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -426,10 +418,6 @@ async fn delete_app(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::DeleteApp { workspace: w_id, path: path.to_owned() },
);
Ok(format!("app {} deleted", path))
}
@@ -437,7 +425,6 @@ async fn delete_app(
async fn update_app(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
@@ -527,14 +514,6 @@ async fn update_app(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateApp {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("app {} updated (npath: {:?})", path, npath))
}

View File

@@ -32,7 +32,6 @@ use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
pub fn workspaced_service() -> Router {
@@ -182,7 +181,6 @@ async fn check_path_conflict<'c>(
async fn create_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
@@ -223,10 +221,6 @@ async fn create_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFlow { workspace: w_id.clone(), path: nf.path.clone() },
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
@@ -286,7 +280,6 @@ async fn update_flow(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
@@ -375,14 +368,6 @@ async fn update_flow(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFlow {
workspace: w_id.clone(),
old_path: flow_path.to_owned(),
new_path: nf.path.clone(),
},
);
let tx = user_db.begin(&authed).await?;
let (dependency_job_uuid, mut tx) = push(
@@ -465,7 +450,6 @@ async fn exists_flow_by_path(
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -490,10 +474,6 @@ async fn archive_flow_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::ArchiveFlow { workspace: w_id, path: path.to_owned() },
);
Ok(format!("Flow {path} archived"))
}

View File

@@ -9,7 +9,6 @@
use crate::{
db::{UserDB, DB},
users::Authed,
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -140,7 +139,6 @@ async fn check_name_conflict<'c>(
async fn create_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
) -> Result<String> {
@@ -195,12 +193,8 @@ async fn create_folder(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateFolder { workspace: w_id, name: ng.name.clone() },
);
tx.commit().await?;
Ok(format!("Created folder {}", ng.name))
}
@@ -251,7 +245,6 @@ pub async fn require_is_owner(
async fn update_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(ng): Json<UpdateFolder>,
) -> Result<String> {
@@ -305,12 +298,8 @@ async fn update_folder(
None,
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone().clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.to_owned() },
);
tx.commit().await?;
Ok(format!("Updated folder {}", name))
}
@@ -427,7 +416,6 @@ async fn get_folder_usage(
async fn delete_folder(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
@@ -452,12 +440,6 @@ async fn delete_folder(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("delete folder at name {}", name))
}
@@ -465,7 +447,6 @@ async fn add_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -496,12 +477,6 @@ async fn add_owner(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Added {} to folder {}", owner, name))
}
@@ -535,7 +510,6 @@ async fn remove_owner(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(Owner { owner }): Json<Owner>,
) -> Result<String> {
@@ -566,11 +540,5 @@ async fn remove_owner(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateFolder { workspace: w_id, name: name.clone() },
);
Ok(format!("Removed {} to folder {}", owner, name))
}

View File

@@ -21,7 +21,6 @@ use crate::{
oauth2::{build_oauth_clients, SlackVerifier},
tracing_init::{MyMakeSpan, MyOnResponse},
users::{Authed, OptAuthed},
webhook_util::WebhookShared,
};
mod apps;
@@ -43,7 +42,6 @@ mod tracing_init;
mod users;
mod utils;
mod variables;
mod webhook_util;
mod worker_ping;
mod workspaces;
@@ -108,8 +106,7 @@ pub async fn run_server(
std::env::var("COOKIE_DOMAIN").ok(),
))))
.layer(Extension(http_client))
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
.layer(CookieManagerLayer::new());
// build our application with a route
let app = Router::new()
.nest(

View File

@@ -9,7 +9,6 @@
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -264,7 +263,6 @@ async fn check_path_conflict<'c>(
async fn create_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
@@ -295,11 +293,6 @@ async fn create_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResource { workspace: w_id, path: resource.path.clone() },
);
Ok((
StatusCode::CREATED,
format!("resource {} created", resource.path),
@@ -309,7 +302,6 @@ async fn create_resource(
async fn delete_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -341,18 +333,12 @@ async fn delete_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResource { workspace: w_id, path: path.to_owned() },
);
Ok(format!("resource {} deleted", path))
}
async fn update_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditResource>,
@@ -414,15 +400,6 @@ async fn update_resource(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("resource {} updated (npath: {:?})", path, npath))
}
@@ -434,7 +411,6 @@ struct UpdateResource {
async fn update_resource_value(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(nv): Json<UpdateResource>,
) -> Result<String> {
@@ -460,14 +436,6 @@ async fn update_resource_value(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResource {
workspace: w_id,
old_path: path.to_owned(),
new_path: path.to_owned(),
},
);
Ok(format!("value of resource {} updated", path))
}
@@ -545,7 +513,6 @@ async fn exists_resource_type(
async fn create_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(resource_type): Json<CreateResourceType>,
) -> Result<(StatusCode, String)> {
@@ -576,11 +543,6 @@ async fn create_resource_type(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateResourceType { name: resource_type.name.clone() },
);
Ok((
StatusCode::CREATED,
format!("resource_type {} created", resource_type.name),
@@ -612,7 +574,6 @@ async fn check_rt_path_conflict<'c>(
async fn delete_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
@@ -637,10 +598,6 @@ async fn delete_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteResourceType { name: name.clone() },
);
Ok(format!("resource_type {} deleted", name))
}
@@ -648,7 +605,6 @@ async fn delete_resource_type(
async fn update_resource_type(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, name)): Path<(String, String)>,
Json(ns): Json<EditResourceType>,
) -> Result<String> {
@@ -678,10 +634,6 @@ async fn update_resource_type(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateResourceType { name: name.clone() },
);
Ok(format!("resource_type {} updated", name))
}

View File

@@ -14,7 +14,6 @@ use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
extract::{Extension, Path, Query},
@@ -185,7 +184,6 @@ fn hash_script(ns: &NewScript) -> i64 {
async fn create_script(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(ns): Json<NewScript>,
@@ -402,14 +400,6 @@ async fn create_script(
Some([("hash", hash.to_string().as_str())].into()),
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateScript {
workspace: w_id,
path: ns.path.clone(),
hash: hash.to_string(),
},
);
} else {
audit_log(
&mut tx,
@@ -427,14 +417,6 @@ async fn create_script(
),
)
.await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateScript {
workspace: w_id,
path: ns.path.clone(),
hash: hash.to_string(),
},
);
}
tx.commit().await?;
@@ -618,7 +600,6 @@ async fn get_deployment_status(
async fn archive_script_by_path(
authed: Authed,
Extension(webhook): Extension<WebhookShared>,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -645,10 +626,6 @@ async fn archive_script_by_path(
)
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(())
}
@@ -656,7 +633,6 @@ async fn archive_script_by_path(
async fn archive_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
@@ -681,18 +657,12 @@ async fn archive_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script))
}
async fn delete_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
@@ -721,11 +691,6 @@ async fn delete_script_by_hash(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteScript { workspace: w_id, hash: hash.to_string() },
);
Ok(Json(script))
}

View File

@@ -12,7 +12,6 @@ use crate::{
db::{UserDB, DB},
oauth2::{AllClients, _refresh_token},
users::{require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
BaseUrl,
};
/*
@@ -225,7 +224,6 @@ async fn check_path_conflict<'c>(
async fn create_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(variable): Json<CreateVariable>,
) -> Result<(StatusCode, String)> {
@@ -267,11 +265,6 @@ async fn create_variable(
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::CreateVariable { workspace: w_id, path: variable.path.clone() },
);
Ok((
StatusCode::CREATED,
format!("variable {} created", variable.path),
@@ -281,7 +274,6 @@ async fn create_variable(
async fn delete_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
@@ -314,11 +306,6 @@ async fn delete_variable(
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::DeleteVariable { workspace: w_id, path: path.to_owned() },
);
Ok(format!("variable {} deleted", path))
}
@@ -333,7 +320,6 @@ struct EditVariable {
async fn update_variable(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditVariable>,
@@ -419,15 +405,6 @@ async fn update_variable(
.await?;
tx.commit().await?;
webhook.send_message(
w_id.clone(),
WebhookMessage::UpdateVariable {
workspace: w_id,
old_path: path.to_owned(),
new_path: npath.clone(),
},
);
Ok(format!("variable {} updated (npath: {:?})", path, npath))
}

View File

@@ -1,112 +0,0 @@
use std::time::Duration;
use serde::Serialize;
use tokio::{select, sync::mpsc, time::interval};
use crate::db::DB;
lazy_static::lazy_static! {
// TODO: these aren't synced, they should be moved into the queue abstraction once/if that happens.
static ref WEBHOOK_REQUEST_COUNT: prometheus::Histogram = prometheus::register_histogram!(
"webhook_request",
"Histogram of webhook requests made"
)
.unwrap();
}
#[derive(Serialize)]
#[serde(tag = "type")]
pub enum WebhookMessage {
// See https://serde.rs/enum-representations.html#internally-tagged for how this looks in JSON
CreateApp { workspace: String, path: String },
DeleteApp { workspace: String, path: String },
UpdateApp { workspace: String, old_path: String, new_path: String },
CreateFlow { workspace: String, path: String },
UpdateFlow { workspace: String, old_path: String, new_path: String },
ArchiveFlow { workspace: String, path: String },
CreateFolder { workspace: String, name: String },
UpdateFolder { workspace: String, name: String },
DeleteFolder { workspace: String, name: String },
DeleteResource { workspace: String, path: String },
CreateResource { workspace: String, path: String },
UpdateResource { workspace: String, old_path: String, new_path: String },
CreateResourceType { name: String },
DeleteResourceType { name: String },
UpdateResourceType { name: String },
CreateScript { workspace: String, path: String, hash: String },
UpdateScript { workspace: String, path: String, hash: String },
DeleteScript { workspace: String, hash: String },
CreateVariable { workspace: String, path: String },
UpdateVariable { workspace: String, old_path: String, new_path: String },
DeleteVariable { workspace: String, path: String },
}
#[derive(Clone)]
pub struct WebhookShared {
pub channel: mpsc::UnboundedSender<(String, WebhookMessage)>,
}
impl WebhookShared {
pub fn new(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>, db: DB) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<(String, WebhookMessage)>();
let _process = tokio::spawn(async move {
let client = reqwest::Client::builder()
// TODO: investigate pool timeouts and such if TCP load is high
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let cache = retainer::Cache::new();
let mut cache_purge_interval = interval(Duration::from_secs(30));
loop {
select! {
biased;
_ = shutdown_rx.recv() => break,
r = rx.recv() => match r {
Some((workspace_id, message)) => {
let url_guard = match cache.get(&workspace_id).await {
Some(guard) => {
guard
},
None => {
let Ok(webook_opt) =
sqlx::query_scalar!(
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
workspace_id
)
.fetch_one(
&db,
)
.await else {
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()
}
};
let webook_opt = url_guard.value();
if let Some(url) = webook_opt {
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
let _ = client.post(url).json(&message).send().await;
timer.stop_and_record();
drop(url_guard);
}
},
None => break,
},
_ = futures::future::poll_fn(|cx| cache_purge_interval.poll_tick(cx)) => {
tracing::trace!("Purging Webhook Cache");
cache.purge(10, 0.50).await;
},
}
}
});
Self { channel: tx }
}
pub fn send_message(&self, workspace_id: String, message: WebhookMessage) {
let _ = self.channel.send((workspace_id.clone(), message));
}
}

View File

@@ -51,7 +51,6 @@ pub fn workspaced_service() -> Router {
.route("/delete_invite", post(delete_invite))
.route("/get_settings", get(get_settings))
.route("/edit_slack_command", post(edit_slack_command))
.route("/edit_webhook", post(edit_webhook))
.route("/edit_auto_invite", post(edit_auto_invite))
.route("/tarball", get(tarball_workspace))
.route("/premium_info", get(premium_info))
@@ -91,7 +90,6 @@ pub struct WorkspaceSettings {
pub auto_invite_operator: Option<bool>,
pub customer_id: Option<String>,
pub plan: Option<String>,
pub webhook: Option<String>,
}
#[derive(FromRow, Serialize, Debug)]
@@ -119,11 +117,6 @@ struct EditAutoInvite {
operator: Option<bool>,
}
#[derive(Deserialize)]
struct EditWebhook {
webhook: Option<String>,
}
#[derive(Deserialize)]
struct CreateWorkspace {
id: String,
@@ -526,48 +519,6 @@ async fn edit_auto_invite(
))
}
async fn edit_webhook(
authed: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Authed { is_admin, username, .. }: Authed,
Json(ew): Json<EditWebhook>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
if let Some(webhook) = &ew.webhook {
sqlx::query!(
"UPDATE workspace_settings SET webhook = $1 WHERE workspace_id = $2",
webhook,
&w_id
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET webhook = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut tx)
.await?;
}
audit_log(
&mut tx,
&authed.username,
"workspaces.edit_webhook",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("webhook", &format!("{:?}", ew.webhook)[..])].into()),
)
.await?;
tx.commit().await?;
Ok(format!("Edit webhook for workspace {}", &w_id))
}
async fn list_workspaces_as_super_admin(
authed: Authed,
Extension(user_db): Extension<UserDB>,

View File

@@ -45,7 +45,7 @@ lazy_static::lazy_static! {
}
const MAX_FREE_EXECS: i32 = 1000;
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
const MAX_FREE_CONCURRENT_RUNS: i32 = 3;
pub async fn cancel_job<'c>(
username: &str,
@@ -82,32 +82,7 @@ pub async fn cancel_job<'c>(
Ok((tx, job_option))
}
pub async fn pull(
db: &Pool<Postgres>,
whitelist_workspaces: Option<Vec<String>>,
blacklist_workspaces: Option<Vec<String>>,
) -> windmill_common::error::Result<Option<QueuedJob>> {
let mut workspaces_filter = String::new();
if let Some(whitelist) = whitelist_workspaces {
workspaces_filter.push_str(&format!(
" AND workspace_id IN ({})",
whitelist
.into_iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(",")
));
}
if let Some(blacklist) = blacklist_workspaces {
workspaces_filter.push_str(&format!(
" AND workspace_id NOT IN ({})",
blacklist
.into_iter()
.map(|x| format!("'{x}'"))
.collect::<Vec<String>>()
.join(",")
));
}
pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<QueuedJob>> {
/* Jobs can be started if they:
* - haven't been started before,
* running = false
@@ -115,7 +90,7 @@ pub async fn pull(
* suspend_until is non-null
* and suspend = 0 when the resume messages are received
* or suspend_until <= now() if it has timed out */
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(&format!(
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(
"UPDATE queue
SET running = true
, started_at = coalesce(started_at, now())
@@ -124,17 +99,17 @@ pub async fn pull(
WHERE id = (
SELECT id
FROM queue
WHERE ((running = false
WHERE ( running = false
AND scheduled_for <= now())
OR (suspend_until IS NOT NULL
AND ( suspend <= 0
OR suspend_until <= now()))) {workspaces_filter}
OR suspend_until <= now()))
ORDER BY scheduled_for
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING *"
))
RETURNING *",
)
.fetch_optional(db)
.await?;

View File

@@ -404,13 +404,6 @@ pub async fn run_worker(
)
.expect("register prometheus metric");
let worker_busy: prometheus::IntGauge = prometheus::register_int_gauge!(prometheus::Opts::new(
"worker_busy",
"Is the worker busy executing a job?",
)
.const_label("name", &worker_name))
.unwrap();
let mut jobs_executed = 0;
let deno_path = std::env::var("DENO_PATH").unwrap_or_else(|_| "/usr/bin/deno".to_string());
@@ -437,12 +430,6 @@ pub async fn run_worker(
let pip_local_dependencies = std::env::var("PIP_LOCAL_DEPENDENCIES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
let whitelist_workspaces = std::env::var("WHITELIST_WORKSPACES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
let blacklist_workspaces = std::env::var("BLACKLIST_WORKSPACES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
let pip_local_dependencies = if pip_local_dependencies == Some(vec!["".to_string()]) {
None
@@ -494,14 +481,11 @@ pub async fn run_worker(
let (same_worker_tx, mut same_worker_rx) = mpsc::channel::<Uuid>(5);
loop {
worker_busy.set(0);
uptime_metric.inc_by(
((Instant::now() - start_time).as_millis() - uptime_metric.get() as u128)
.try_into()
.unwrap(),
);
let do_break = async {
if last_ping.elapsed().as_secs() > NUM_SECS_ENV_CHECK {
sqlx::query!(
@@ -542,9 +526,7 @@ pub async fn run_worker(
.await
.map_err(|_| Error::InternalErr("Impossible to fetch same_worker job".to_string())))
},
(job, timer) = {
let timer = worker_pull_duration.start_timer();
pull(&db, whitelist_workspaces.clone(), blacklist_workspaces.clone()).map(|x| (x, timer)) } => {
(job, timer) = {let timer = worker_pull_duration.start_timer(); pull(&db).map(|x| (x, timer)) } => {
drop(timer);
(false, job)
},
@@ -554,7 +536,6 @@ pub async fn run_worker(
return true;
}
worker_busy.set(1);
match next_job {
Ok(Some(job)) => {
let label_values = [
@@ -1790,13 +1771,12 @@ inner_script = __import__("inner")
with open("args.json") as f:
kwargs = json.load(f, strict=False)
for k, v in list(kwargs.items()):
if v == '<function call>':
del kwargs[k]
args = {{}}
{spread}
{transforms}
for k, v in list(args.items()):
if v == '<function call>':
del args[k]
try:
res = inner_script.main(**args)
res_json = json.dumps(res, separators=(',', ':'), default=str).replace('\n', '')

View File

@@ -1,12 +1,10 @@
// deno-lint-ignore-file no-explicit-any
import { colors, setClient, UserService } from "./deps.ts";
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
import { colors, setClient } from "./deps.ts";
import { tryGetLoginInfo } from "./login.ts";
import { GlobalOptions } from "./types.ts";
import {
addWorkspace,
getActiveWorkspace,
getWorkspaceByName,
removeWorkspace,
Workspace,
} from "./workspace.ts";
@@ -69,27 +67,6 @@ export async function requireLogin(opts: GlobalOptions) {
}
setClient(token, workspace.remote.substring(0, workspace.remote.length - 1));
try {
await UserService.globalWhoami();
} catch {
console.log(
"! Could not reach API given existing credentials. Attempting to reauth...",
);
const newToken = await loginInteractive(workspace.remote);
if (!newToken) {
throw new Error("Could not reauth");
}
removeWorkspace(workspace.name);
workspace.token = newToken;
addWorkspace(workspace);
setClient(
token,
workspace.remote.substring(0, workspace.remote.length - 1),
);
await UserService.globalWhoami();
}
}
export async function tryResolveVersion(

View File

@@ -1,8 +0,0 @@
// globally shared decoverto instance
import { Decoverto } from "npm:decoverto";
const decoverto = new Decoverto();
// TODO: Properly type FlowModule
export { Any, array, map, MapShape, model, property } from "npm:decoverto";
export { decoverto };

View File

@@ -1,40 +1,32 @@
// windmill
export { setClient } from "https://deno.land/x/windmill@v1.61.1/mod.ts";
export * from "https://deno.land/x/windmill@v1.61.1/windmill-api/index.ts";
export { setClient } from "https://deno.land/x/windmill@v1.56.0/mod.ts";
export * from "https://deno.land/x/windmill@v1.56.0/windmill-api/index.ts";
// cliffy
export { Command } from "https://deno.land/x/cliffy@v0.25.7/command/command.ts";
export { Table } from "https://deno.land/x/cliffy@v0.25.7/table/table.ts";
export { colors } from "https://deno.land/x/cliffy@v0.25.7/ansi/colors.ts";
export { Secret } from "https://deno.land/x/cliffy@v0.25.7/prompt/secret.ts";
export { Select } from "https://deno.land/x/cliffy@v0.25.7/prompt/select.ts";
export { Confirm } from "https://deno.land/x/cliffy@v0.25.7/prompt/confirm.ts";
export { Input } from "https://deno.land/x/cliffy@v0.25.7/prompt/input.ts";
export { Command } from "https://deno.land/x/cliffy@v0.25.6/command/command.ts";
export { Table } from "https://deno.land/x/cliffy@v0.25.6/table/table.ts";
export { colors } from "https://deno.land/x/cliffy@v0.25.6/ansi/colors.ts";
export { Secret } from "https://deno.land/x/cliffy@v0.25.6/prompt/secret.ts";
export { Select } from "https://deno.land/x/cliffy@v0.25.6/prompt/select.ts";
export { Confirm } from "https://deno.land/x/cliffy@v0.25.6/prompt/confirm.ts";
export { Input } from "https://deno.land/x/cliffy@v0.25.6/prompt/input.ts";
export {
DenoLandProvider,
UpgradeCommand,
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
} from "https://deno.land/x/cliffy@v0.25.6/command/upgrade/mod.ts";
// std
export { Untar } from "https://deno.land/std@0.176.0/archive/untar.ts";
export * as path from "https://deno.land/std@0.176.0/path/mod.ts";
export { ensureDir } from "https://deno.land/std@0.176.0/fs/ensure_dir.ts";
export { Untar } from "https://deno.land/std@0.170.0/archive/untar.ts";
export * as path from "https://deno.land/std@0.170.0/path/mod.ts";
export { ensureDir } from "https://deno.land/std@0.170.0/fs/ensure_dir.ts";
export {
copy,
readAll,
readerFromStreamReader,
} from "https://deno.land/std@0.176.0/streams/mod.ts";
export { DelimiterStream } from "https://deno.land/std@0.176.0/streams/mod.ts";
export { iterateReader } from "https://deno.land/std@0.176.0/streams/iterate_reader.ts";
} from "https://deno.land/std@0.170.0/streams/mod.ts";
export { DelimiterStream } from "https://deno.land/std@0.170.0/streams/mod.ts";
// other
export { getAvailablePort } from "https://deno.land/x/port@1.0.0/mod.ts";
export { default as dir } from "https://deno.land/x/dir@1.5.1/mod.ts";
export { passwordGenerator } from "https://deno.land/x/password_generator@latest/mod.ts"; // TODO: I think the version is called latest, but it's still pinned.
export { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
export * as cbor from "https://deno.land/x/cbor@v1.4.1/index.js";
export { default as Murmurhash3 } from "https://deno.land/x/murmurhash@v1.0.0/mod.ts";
export {
default as microdiff,
} from "https://deno.land/x/microdiff@v1.3.1/index.ts";
export { default as objectHash } from "https://deno.land/x/object_hash@2.0.3.1/mod.ts";

View File

@@ -1,145 +1,16 @@
// deno-lint-ignore-file no-explicit-any
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import { GlobalOptions } from "./types.ts";
import {
colors,
Command,
Flow,
FlowModule,
FlowService,
JobService,
microdiff,
OpenFlowWPath,
OpenFlow,
Table,
} from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { resolve, track_job } from "./script.ts";
import { Any, array, decoverto, model, property } from "./decoverto.ts";
@model()
export class FlowValueFilePart {
@property(array(Any))
modules: Array<FlowModule>;
@property(Any)
failure_module?: FlowModule;
@property(() => Boolean)
same_worker?: boolean;
constructor(modules: Array<FlowModule>) {
this.modules = modules;
}
}
// this is effectively "OpenFlow" but a copy as it is accepted by the CLI
@model()
export class FlowFile implements Resource, PushDiffs {
@property(() => String)
summary: string;
@property(() => String)
description?: string;
@property(() => FlowValueFilePart)
value: FlowValueFilePart;
@property(Any)
schema?: any;
constructor(summary: string, value: FlowValueFilePart) {
this.summary = summary;
this.value = value;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (
await FlowService.existsFlowByPath({
workspace: workspace,
path: remotePath,
})
) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing flow...`,
),
);
// TODO: Make these optional in backend (not path ofc)
const changeset: OpenFlowWPath = {
path: remotePath,
summary: this.summary,
value: this.value,
description: this.description, // This is OpenAPIed as optional, but isn't
schema: this.schema, // Same
};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
!["summary", "description", "schema"].includes(
diff.path[0] as string,
)
)
)
) {
throw new Error("Invalid flow diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.bold.yellow("Creating new flow..."));
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath,
summary: this.summary,
value: this.value,
schema: this.schema,
description: this.description,
},
});
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let remote: Flow | undefined;
try {
remote = await FlowService.getFlowByPath({
workspace,
path: remotePath,
});
} catch {
remote = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(remote ?? {}, this, { cyclesFix: false }),
);
}
}
type Options = GlobalOptions;
@@ -159,10 +30,38 @@ export async function pushFlow(
workspace: string,
remotePath: string,
) {
const data = decoverto.type(FlowFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, remotePath);
const data: OpenFlow = JSON.parse(await Deno.readTextFile(filePath));
if (
await FlowService.existsFlowByPath({
workspace: workspace,
path: remotePath,
})
) {
console.log(colors.bold.yellow("Updating existing flow..."));
await FlowService.updateFlow({
workspace: workspace,
path: remotePath,
requestBody: {
path: remotePath,
summary: data.summary,
value: data.value,
schema: data.schema,
description: data.description,
},
});
} else {
console.log(colors.bold.yellow("Creating new flow..."));
await FlowService.createFlow({
workspace: workspace,
requestBody: {
path: remotePath,
summary: data.summary,
value: data.value,
schema: data.schema,
description: data.description,
},
});
}
}
async function list(opts: GlobalOptions & { showArchived?: boolean }) {

View File

@@ -1,122 +1,6 @@
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
import { colors, Command, Folder, FolderService } from "./deps.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import {
array,
decoverto,
map,
MapShape,
model,
property,
} from "./decoverto.ts";
@model()
export class FolderFile implements Resource, PushDiffs {
@property(array(() => String))
owners: Array<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
extra_perms: Map<string, boolean> | undefined;
async push(workspace: string, remotePath: string): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
}
if (remotePath.startsWith("f/")) {
remotePath = remotePath.substring(2);
}
let existing: Folder | undefined;
try {
existing = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
}
if (remotePath.startsWith("f/")) {
remotePath = remotePath.substring(2);
}
// TODO: Support this in backend
let exists: boolean;
try {
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
} catch {
exists = false;
}
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder...`,
),
);
const changeset: {
owners?: string[] | undefined;
extra_perms?: any;
} = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["owners", "extra_perms"].includes(diff.path[0] as string)
)
) {
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.bold.yellow("Creating new folder..."));
await FolderService.createFolder({
workspace: workspace,
requestBody: {
name: remotePath,
extra_perms: this.extra_perms,
owners: this.owners,
},
});
}
}
}
import { GlobalOptions } from "./types.ts";
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
@@ -137,15 +21,61 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
console.log(colors.bold.underline.green("Resource successfully pushed"));
}
type FolderFile = {
owners: Array<string> | undefined;
extra_perms: Record<string, boolean> | undefined;
};
export async function pushFolder(
workspace: string,
filePath: string,
remotePath: string,
) {
const data = decoverto.type(FolderFile).rawToInstance(
await Deno.readTextFile(filePath),
);
data.push(workspace, remotePath);
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
}
if (remotePath.startsWith("f/")) {
remotePath = remotePath.substring(2);
}
const data: FolderFile = JSON.parse(await Deno.readTextFile(filePath));
let optFolder: Folder | undefined;
try {
optFolder = await FolderService.getFolder({ workspace, name: remotePath });
} catch {
optFolder = undefined;
}
if (optFolder) {
// for (const [k, v] of Object.entries(optFolder.extra_perms)) {
// if (!data.extra_perms || data.extra_perms[k] !== v) {
// console.log(colors.red.underline.bold(`Extra Perms missmatch on ${k}`));
// return;
// }
// }
console.log(colors.yellow("Updating existing folder..."));
await FolderService.updateFolder({
workspace,
name: remotePath,
requestBody: {
extra_perms: data.extra_perms,
owners: data.owners,
},
});
} else {
console.log(colors.yellow("Creating new folder..."));
await FolderService.createFolder({
workspace,
requestBody: {
name: remotePath,
extra_perms: data.extra_perms,
owners: data.owners,
},
});
// HACK: Workaround backend automatically adding current user to folder.
await pushFolder(workspace, filePath, remotePath);
}
}
const command = new Command()

View File

@@ -1,6 +1,6 @@
import { Command } from "./deps.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import { pushResourceTypeDef } from "./resource-type.ts";
import { GlobalOptions } from "./types.ts";
async function pull(opts: GlobalOptions) {
@@ -56,10 +56,14 @@ async function pull(opts: GlobalOptions) {
const x of list
) {
console.log("syncing " + x.name);
const f = new ResourceTypeFile();
f.description = x.description;
f.schema = JSON.parse(x.schema);
await f.push(workspace.workspaceId, x.name);
await pushResourceTypeDef(
workspace.workspaceId,
x.name,
{
description: x.description,
schema: JSON.parse(x.schema),
},
);
}
}

View File

@@ -8,14 +8,13 @@ import variable from "./variable.ts";
import push from "./push.ts";
import pull from "./pull.ts";
import hub from "./hub.ts";
import folder from "./folder.ts";
import sync from "./sync.ts";
// import folder from "./folder.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
const VERSION = "v1.62.0";
const VERSION = "v1.61.1";
let command: any = new Command()
const command = new Command()
.name("wmill")
.description("A simple CLI tool for windmill.")
.globalOption(
@@ -33,9 +32,10 @@ let command: any = new Command()
.command("resource", resource)
.command("user", user)
.command("variable", variable)
.command("push", push)
.command("pull", pull)
.command("hub", hub)
.command("folder", folder)
.command("sync", sync)
// .command("folder", folder)
.command("version", "Show version information")
.action(async (opts) => {
console.log("CLI build against " + VERSION);
@@ -61,12 +61,6 @@ let command: any = new Command()
}),
);
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
command = command
.command("push", push)
.command("pull", pull);
}
try {
await command.parse(Deno.args);
} catch (e) {

View File

@@ -1,11 +1,20 @@
// deno-lint-ignore-file no-explicit-any
import { resolveWorkspace } from "./context.ts";
import { GlobalOptions } from "./types.ts";
import { colors, Command, readerFromStreamReader, Untar } from "./deps.ts";
import { Workspace } from "./workspace.ts";
import {
colors,
Command,
Confirm,
copy,
ensureDir,
path,
readerFromStreamReader,
Untar,
} from "./deps.ts";
async function pull(opts: GlobalOptions & { override: boolean }, dir: string) {
const workspace = await resolveWorkspace(opts);
export async function downloadTar(
workspace: Workspace,
): Promise<Untar | undefined> {
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set("Authorization", "Bearer " + workspace.token);
requestHeaders.set("Content-Type", "application/octet-stream");
@@ -26,29 +35,51 @@ export async function downloadTar(
),
);
console.log(await tarResponse.text());
return undefined;
return;
}
const streamReader = tarResponse.body?.getReader();
if (!streamReader) {
console.log(colors.red("Failed to read tar request body"));
return undefined;
return;
}
console.log(colors.yellow("Streaming tarball to disk..."));
const denoReader = readerFromStreamReader(streamReader);
const untar = new Untar(denoReader);
return untar;
}
async function stub(
_opts: GlobalOptions & { override: boolean },
_dir: string,
) {
console.log(
colors.red.underline(
'Pull is deprecated. Use "sync pull --raw" instead. See <TODO_LINK_HERE> for more information.',
),
);
for await (const entry of untar) {
console.log(entry.fileName);
const filePath = path.resolve(dir, entry.fileName);
if (entry.type === "directory") {
await ensureDir(filePath);
continue;
}
await ensureDir(path.dirname(filePath));
if (!opts.override) {
let exists = false;
try {
const _stat = await Deno.stat(filePath);
exists = true;
} catch {
exists = false;
}
if (exists) {
if (
!(await Confirm.prompt(
"Conflict at " +
filePath +
" do you want to override the local version?",
))
) {
continue;
}
}
}
const file = await Deno.open(filePath, { write: true, create: true });
const len = await copy(entry, file);
await file.truncate(len);
file.close();
}
console.log(colors.green("Done. Wrote all files to disk."));
}
const command = new Command()
@@ -56,6 +87,6 @@ const command = new Command()
"Pull all definitions in the current workspace from the API and write them to disk.",
)
.arguments("<dir:string>")
.action(stub as any);
.action(pull as any);
export default command;

View File

@@ -1,21 +1,266 @@
// deno-lint-ignore-file no-explicit-any
import { colors, Command } from "./deps.ts";
import { colors, Command, path } from "./deps.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import { pushFlow } from "./flow.ts";
import { pushResource } from "./resource.ts";
import { findContentFile, pushScript } from "./script.ts";
import { GlobalOptions } from "./types.ts";
import { pushVariable } from "./variable.ts";
import { pushResourceType } from "./resource-type.ts";
import { pushFolder } from "./folder.ts";
async function stub(
_opts: GlobalOptions,
_dir?: string,
) {
type Candidate = {
path: string;
namespaceKind: "user" | "group" | "folder";
namespaceName: string;
};
type ResourceTypeCandidate = {
path: string;
};
type FolderCandidate = {
path: string;
namespaceName: string;
};
async function findCandidateFiles(
dir: string,
): Promise<
{
normal: Candidate[];
resourceTypes: ResourceTypeCandidate[];
folders: FolderCandidate[];
}
> {
dir = path.resolve(dir);
if (path.dirname(dir).startsWith(".")) {
return { normal: [], resourceTypes: [], folders: [] };
}
const normalCandidates: Candidate[] = [];
const resourceTypeCandidates: ResourceTypeCandidate[] = [];
const folderCandidates: FolderCandidate[] = [];
for await (const e of Deno.readDir(dir)) {
if (e.isDirectory) {
if (e.name == "u" || e.name == "g" || e.name == "f") { // TODO: Check version for f
const newDir = dir + (dir.endsWith("/") ? "" : "/") + e.name;
for await (const e2 of Deno.readDir(newDir)) {
if (e2.isDirectory) {
if (e2.name.startsWith(".")) continue;
const namespaceName = e2.name;
const stack: string[] = [];
{
const path = newDir + "/" + namespaceName + "/";
stack.push(path);
try {
await Deno.stat(path + "folder.meta.json");
folderCandidates.push({
namespaceName,
path: path + "folder.meta.json",
});
} catch {}
}
while (stack.length > 0) {
const dir2 = stack.pop()!;
for await (const e3 of Deno.readDir(dir2)) {
if (e3.isFile) {
if (e3.name === "folder.meta.json") continue;
normalCandidates.push({
path: dir2 + e3.name,
namespaceKind: e.name == "g"
? "group"
: e.name == "u"
? "user"
: "folder",
namespaceName: namespaceName,
});
} else {
stack.push(dir2 + e3.name + "/");
}
}
}
}
}
} else {
console.log(
colors.yellow(
"Including organizational folder " + e.name + " in push!",
),
);
const { normal, resourceTypes, folders } = await findCandidateFiles(
path.join(dir, e.name),
);
normalCandidates.push(...normal);
resourceTypeCandidates.push(...resourceTypes);
folderCandidates.push(...folders);
}
} else {
// handle root files
if (e.name.endsWith(".resource-type.json")) {
resourceTypeCandidates.push({
path: dir + (dir.endsWith("/") ? "" : "/") + e.name,
});
}
}
}
return {
normal: normalCandidates,
folders: folderCandidates,
resourceTypes: resourceTypeCandidates,
};
}
async function push(opts: GlobalOptions, dir?: string) {
dir = dir ?? Deno.cwd();
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log(colors.blue("Searching Directory..."));
const { normal, resourceTypes, folders } = await findCandidateFiles(dir);
console.log(
colors.red.underline(
'Push is deprecated. Use "sync push --raw" instead. See <TODO_LINK_HERE> for more information.',
colors.blue(
"Found " + (normal.length + resourceTypes.length + folders.length) +
" candidates",
),
);
for (const resourceType of resourceTypes) {
const fileName = resourceType.path.substring(
resourceType.path.lastIndexOf("/") + 1,
);
const fileNameParts = fileName.split(".");
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + resourceType.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + resourceType.path));
continue;
}
console.log("pushing resource type " + fileNameParts.at(-3)!);
await pushResourceType(
workspace.workspaceId,
resourceType.path,
fileNameParts.at(-3)!,
);
}
for (const folder of folders) {
await pushFolder(
workspace.workspaceId,
folder.path,
"f/" + folder.namespaceName,
);
}
for (const candidate of normal) {
// full file name. No leading /. includes .type.json
const fileName = candidate.path.substring(
candidate.path.lastIndexOf("/") + 1,
);
// figure out just the path after ...../u|g/username|group/ (in extra dir)
const dirParts = candidate.path.split("/").filter((x) => x.length > 0);
// TODO: check version for folder
const gIndex = dirParts.findIndex((x) => x == "u" || x == "g" || x == "f");
const extraDir = dirParts.slice(gIndex + 2, -1).join("/");
// file name parts has .json (hopefully) at -1, type at -2, and the actual name at -3. Dots in names are not allowed.
const fileNameParts = fileName.split(".");
// filter out script content files
if (
fileNameParts.at(-1) == "ts" ||
fileNameParts.at(-1) == "py" ||
fileNameParts.at(-1) == "go"
) {
// probably part of a script. Silent ignore.
continue;
}
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + candidate.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + candidate.path));
continue;
}
// get the type & filter it for valid ones.
const type = fileNameParts.at(-2);
if (type == "resource-type") {
console.log(
colors.yellow(
"Found resource type file at " +
candidate.path +
" this appears to be inside a path folder. Resource types are not addressed by path. Place them at the root or inside only an organizational folder. Ignoring this file!",
),
);
continue;
}
if (
type != "flow" &&
type != "resource" &&
type != "script" &&
type != "variable"
) {
console.log(
colors.yellow(
"file with invalid type " + type + " found at " + candidate.path,
),
);
continue;
}
// create the remotePath for the API
const remotePath = (candidate.namespaceKind === "group"
? "g/"
: (candidate.namespaceKind === "user" ? "u/" : "f/")) +
candidate.namespaceName +
"/" +
(extraDir.length > 0 ? extraDir + "/" : "") +
fileNameParts.at(-3);
console.log("pushing " + type + " to " + remotePath);
if (type == "flow") {
await pushFlow(candidate.path, workspace.workspaceId, remotePath);
} else if (type == "resource") {
await pushResource(workspace.workspaceId, candidate.path, remotePath);
} else if (type == "script") {
let contentPath: string;
try {
contentPath = await findContentFile(candidate.path);
} catch (e) {
console.log(colors.red(e.toString()));
continue;
}
await pushScript(
candidate.path,
contentPath,
workspace.workspaceId,
remotePath,
);
} else if (type == "variable") {
await pushVariable(workspace.workspaceId, candidate.path, remotePath);
}
}
console.log(colors.underline.bold.green("Successfully Pushed all files."));
}
const command = new Command()
.description("Push all files from a folder")
.arguments("[dir:string]")
.action(stub as any);
.action(push as any);
export default command;

View File

@@ -1,129 +1,65 @@
// deno-lint-ignore-file no-explicit-any
import {
Difference,
GlobalOptions,
PushDiffs,
Resource as ResourceI,
setValueByPath,
} from "./types.ts";
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace } from "./context.ts";
import {
colors,
Command,
EditResourceType,
microdiff,
ResourceService,
ResourceType,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
import { colors, Command, ResourceService, Table } from "./deps.ts";
@model()
export class ResourceTypeFile implements ResourceI, PushDiffs {
@property(Any)
type ResourceTypeFile = {
schema?: any;
@property(() => String)
description?: string;
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ResourceType | undefined;
try {
existing = await ResourceService.getResourceType({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (
await ResourceService.existsResourceType({
workspace: workspace,
path: remotePath,
})
) {
if (
(await ResourceService.listResourceType({ workspace })).findIndex((x) =>
x.name === remotePath
) === -1
) {
console.log(
"Resource type " + remotePath +
" is already taken for the current workspace, but cannot be updated. Is this a conflict with starter?",
);
return;
}
console.log(
colors.yellow(
`Applying ${diffs.length} diffs to existing resource type...`,
),
);
const changeset: EditResourceType = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["schema", "description"].includes(diff.path[0] as string)
)
) {
throw new Error("Invalid resource type diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await ResourceService.updateResourceType({
workspace: workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.yellow("Creating new resource type..."));
await ResourceService.createResourceType({
workspace: workspace,
requestBody: {
name: remotePath,
description: this.description,
schema: this.schema,
workspace_id: workspace,
},
});
}
}
}
};
export async function pushResourceType(
workspace: string,
filePath: string,
name: string,
) {
const data: ResourceTypeFile = decoverto.type(ResourceTypeFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, name);
const data: ResourceTypeFile = JSON.parse(await Deno.readTextFile(filePath));
await pushResourceTypeDef(workspace, name, data);
}
export async function pushResourceTypeDef(
workspace: string,
name: string,
data: ResourceTypeFile,
) {
if (
await ResourceService.existsResourceType({
workspace: workspace,
path: name,
})
) {
console.log(colors.yellow("Updating existing resource type..."));
if (
(await ResourceService.listResourceType({ workspace })).findIndex((x) =>
x.name === name
) === -1
) {
console.log(
"Resource type " + name +
" is already taken for the current workspace, but cannot be updated. Is this a conflict with starter?",
);
return;
}
await ResourceService.updateResourceType({
workspace: workspace,
path: name,
requestBody: {
description: data.description,
schema: data.schema,
},
});
} else {
console.log(colors.yellow("Creating new resource type..."));
await ResourceService.createResourceType({
workspace: workspace,
requestBody: {
name: name,
description: data.description,
schema: data.schema,
workspace_id: workspace,
},
});
}
}
type PushOptions = GlobalOptions;

View File

@@ -1,142 +1,80 @@
import {
Difference,
GlobalOptions,
PushDiffs,
Resource as Resource2,
setValueByPath,
} from "./types.ts";
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
colors,
Command,
EditResource,
microdiff,
Resource,
ResourceService,
Table,
} from "./deps.ts";
import { Any, decoverto, model, property } from "./decoverto.ts";
import { colors, Command, Resource, ResourceService, Table } from "./deps.ts";
@model()
export class ResourceFile implements Resource2, PushDiffs {
@property(Any)
value?: any;
@property(() => String)
type ResourceFile = {
value: any;
description?: string;
@property(() => String)
resource_type: string;
@property(() => Boolean)
is_oauth?: boolean; // deprecated
constructor(resource_type: string) {
this.resource_type = resource_type;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (
await ResourceService.existsResource({
workspace: workspace,
path: remotePath,
})
) {
console.log(
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
);
const changeset: EditResource = {
path: remotePath, // TODO: Remove this in backend
};
for (const diff of diffs) {
if (diff.path[0] === "is_oauth") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
continue;
}
if (
diff.type !== "REMOVE" &&
(
diff.path[0] !== "value" && (
diff.path.length !== 1 ||
diff.path[0] !== "description"
)
)
) {
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await ResourceService.updateResource({
workspace: workspace,
path: remotePath,
requestBody: changeset,
});
} else {
if (typeof this.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
}
console.log(colors.yellow("Creating new resource..."));
await ResourceService.createResource({
workspace: workspace,
requestBody: {
path: remotePath,
resource_type: this.resource_type,
value: this.value,
description: this.description,
},
});
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: Resource | undefined;
try {
existing = await ResourceService.getResource({
workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}
};
export async function pushResource(
workspace: string,
filePath: string,
remotePath: string,
) {
const data = decoverto.type(ResourceFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, remotePath);
const data: ResourceFile = JSON.parse(await Deno.readTextFile(filePath));
if (
await ResourceService.existsResource({
workspace: workspace,
path: remotePath,
})
) {
console.log(colors.yellow("Updating existing resource..."));
const existing = await ResourceService.getResource({
workspace: workspace,
path: remotePath,
});
if (existing.resource_type != data.resource_type) {
console.log(
colors.red.underline.bold(
"Remote resource at " +
remotePath +
" exists & has a different resource type. This cannot be updated. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
if (typeof data.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
}
await ResourceService.updateResource({
workspace: workspace,
path: remotePath,
requestBody: {
path: remotePath,
value: data.value,
description: data.description,
},
});
} else {
if (typeof data.is_oauth !== "undefined") {
console.log(
colors.yellow(
"! is_oauth has been removed in newer versions. Ignoring.",
),
);
}
console.log(colors.yellow("Creating new resource..."));
await ResourceService.createResource({
workspace: workspace,
requestBody: {
path: remotePath,
resource_type: data.resource_type,
value: data.value,
description: data.description,
},
});
}
}
type PushOptions = GlobalOptions;

View File

@@ -2,52 +2,20 @@
import { GlobalOptions } from "./types.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
colors,
Command,
JobService,
readAll,
Script,
ScriptService,
Table,
} from "./deps.ts";
import { Any, array, decoverto, model, property } from "./decoverto.ts";
} from "https://deno.land/x/windmill@v1.50.0/windmill-api/index.ts";
import { colors, Command, readAll, ScriptService, Table } from "./deps.ts";
@model()
export class ScriptFile {
@property(() => String)
type ScriptFile = {
parent_hash?: string;
@property(() => String)
summary: string;
@property(() => String)
description: string;
@property(Any)
schema?: any;
@property(() => Boolean)
is_template?: boolean;
@property(array(() => String))
lock?: Array<string>;
@property({
toInstance: (data) => {
if (data == null) return data;
if (
data === "script" || data === "failure" || data === "trigger" ||
data === "command" || data === "approvial"
) {
return data;
}
throw new Error("Invalid kind " + data);
},
toPlain: (data) => data,
})
kind?: "script" | "failure" | "trigger" | "command" | "approval";
constructor(summary: string, description: string) {
this.summary = summary;
this.description = description;
}
}
};
type PushOptions = GlobalOptions;
async function push(
@@ -80,12 +48,10 @@ async function push(
}
export async function findContentFile(filePath: string) {
console.log("Searching " + filePath);
const candidates = [
filePath.replace(".script.json", ".ts"),
filePath.replace(".script.json", ".py"),
filePath.replace(".script.json", ".go"),
filePath.replace(".script.json", ".sh"),
];
const validCandidates = (
await Promise.all(
@@ -113,35 +79,23 @@ export async function findContentFile(filePath: string) {
return validCandidates[0];
}
export function inferContentTypeFromFilePath(
contentPath: string,
): "python3" | "deno" | "go" | "bash" {
let language = contentPath.substring(contentPath.lastIndexOf("."));
if (language == ".ts") language = "deno";
if (language == ".py") language = "python3";
if (language == ".sh") language = "bash";
if (language == ".go") language = "go";
if (
language != "python3" && language != "deno" && language != "go" &&
language != "bash"
) {
throw new Error("Invalid language: " + language);
}
return language;
}
export async function pushScript(
filePath: string,
contentPath: string,
workspace: string,
remotePath: string,
) {
const data = decoverto.type(ScriptFile).rawToInstance(
await Deno.readTextFile(filePath),
);
const data: ScriptFile = JSON.parse(await Deno.readTextFile(filePath));
const content = await Deno.readTextFile(contentPath);
const language = inferContentTypeFromFilePath(contentPath);
let language = contentPath.substring(contentPath.lastIndexOf("."));
if (language == ".ts") language = "deno";
if (language == ".py") language = "python3";
if (language == ".go") language = "go";
if (language != "python3" && language != "deno" && language != "go") {
throw new Error("Invalid language: " + language);
}
let parent_hash = data.parent_hash;
if (!parent_hash) {
try {

View File

@@ -1,969 +0,0 @@
import { requireLogin, resolveWorkspace } from "./context.ts";
import { getWorkspaceStream, Workspace } from "./workspace.ts";
import { decoverto, map, MapShape, model, property } from "./decoverto.ts";
import {
cbor,
colors,
Command,
Confirm,
copy,
ensureDir,
iterateReader,
microdiff,
nanoid,
objectHash,
path,
ScriptService,
} from "./deps.ts";
import {
Difference,
getTypeStrFromPath,
GlobalOptions,
inferTypeFromPath,
setValueByPath,
} from "./types.ts";
import { downloadTar } from "./pull.ts";
import { FolderFile } from "./folder.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import {
findContentFile,
inferContentTypeFromFilePath,
pushScript,
ScriptFile,
} from "./script.ts";
import { ResourceFile } from "./resource.ts";
import { FlowFile } from "./flow.ts";
import { VariableFile } from "./variable.ts";
type TrackedId = string;
const TrackedId = String;
const CONTENT_ENCODER: cbor.Encoder = new cbor.Encoder({ pack: true });
export class Tracked {
#id: TrackedId;
#parent: State;
path: string;
#content_cache?: string;
constructor(id: TrackedId, parent: State, path: string) {
this.#id = id;
this.#parent = parent;
this.path = path;
}
async getContent(): Promise<unknown | undefined> {
if (this.#content_cache) {
return this.#content_cache;
}
if (!this.#parent.stateRoot) {
throw new Error("Parent uninitialized");
}
const f = this.#parent.contentFile(this.#id);
if (!f) {
return undefined;
}
const data = await Deno.readFile(
path.join(this.#parent.stateRoot, ".wmill", f),
);
const content = CONTENT_ENCODER.decode(data);
this.#content_cache = content;
return content;
}
getHash(): string {
return this.#parent.hashes.get(this.#id)!;
}
getId(): TrackedId {
return this.#id;
}
}
@model()
export class State {
// TODO: Handle script contents, as they are separate files & need to be tracked separately.
// I think the way this makes most sense is actually reating them as a separate kind of file everywhere
// And then move to using a diff-based push system everywhere. This should greatly simplify code & enable this easily
@property(map(() => TrackedId, () => String, { shape: MapShape.Object }))
hashes: Map<TrackedId, string>;
@property(map(() => TrackedId, () => String, { shape: MapShape.Object }))
contentFiles: Map<TrackedId, string>;
@property(map(() => String, () => TrackedId, { shape: MapShape.Object }))
tracked: Map<string, TrackedId>;
@property(() => String)
workspaceId: string;
@property(() => String)
remoteUrl: string;
stateRoot?: string;
constructor(
hashes: Map<TrackedId, string>,
tracked: Map<string, TrackedId>,
contentFiles: Map<TrackedId, string>,
workspaceId: string,
remoteUrl: string,
) {
this.hashes = hashes;
this.tracked = tracked;
this.contentFiles = contentFiles;
this.workspaceId = workspaceId;
this.remoteUrl = remoteUrl;
}
add(path: string) {
if (this.tracked.get(path)) {
throw new Error("Cannot newly track already tracked paths");
} else {
this.tracked.set(path, nanoid());
}
}
public forget(path: string) {
const id = this.tracked.get(path);
if (id) {
this.tracked.delete(path);
this.hashes.delete(id);
}
}
public contentFile(id: TrackedId): string | undefined {
return this.contentFiles.get(id);
}
public get(path: string): Tracked {
const id = this.tracked.get(path);
if (!id) {
throw new Error("Could not resolve path " + path);
}
return new Tracked(id, this, path);
}
public async save(): Promise<void> {
if (!this.stateRoot) {
throw new Error("Uninitialized state root");
}
const encoder = new cbor.Encoder({});
const plain = decoverto.type(State).instanceToPlain(this);
const result: Uint8Array = encoder.encode(plain, {});
await Deno.writeFile(path.join(this.stateRoot, ".wmill", "main"), result, {
create: true,
});
}
public static async loadState(dir: string): Promise<State> {
const source = await Deno.readFile(path.join(dir, ".wmill", "main"));
const encoder = new cbor.Encoder({});
const raw = encoder.decode(source);
const state = decoverto.type(State).plainToInstance(
raw,
);
state.stateRoot = dir;
return Object.freeze(state);
}
}
async function getState(opts: GlobalOptions) {
const existingState = await State.loadState(Deno.cwd());
const workspaceStream = await getWorkspaceStream();
const reader = workspaceStream.getReader();
while (true) {
const res = await reader.read();
if (res.value) {
if (
new URL(res.value.remote) == new URL(existingState.remoteUrl) &&
res.value.workspaceId == existingState.workspaceId
) {
opts.workspace = res.value.name;
(opts as any).__secret_workspace = res.value;
break;
}
}
if (res.done) {
break;
}
}
return existingState;
}
async function updateStateFromRemote(
workspace: Workspace,
state: State,
callback: (filename: string) => PromiseLike<boolean> | boolean,
) {
const untar = await downloadTar(workspace);
if (!untar) throw new Error("Failed to pull Tar");
const decoder = new TextDecoder();
for await (const entry of untar) {
const id = state.tracked.get(entry.fileName);
if (id) {
let val = "";
for await (const e of iterateReader(entry)) {
const tmp = decoder.decode(e);
val += tmp;
}
if (entry.fileName.endsWith(".json")) {
const parsed = JSON.parse(val);
const typed = inferTypeFromPath(entry.fileName, parsed);
const oldHash = state.hashes.get(id);
const newHash = objectHash(typed);
if (!oldHash || oldHash !== newHash) {
if (!await callback(entry.fileName)) {
return; // notice that we are not saving
}
state.hashes.set(id, newHash);
const encoded = CONTENT_ENCODER.encode(typed);
const fileName = nanoid();
await Deno.writeFile(
path.join(state.stateRoot!, ".wmill", fileName),
encoded,
{ create: true },
);
state.contentFiles.set(id, fileName);
}
} else {
const fileName = nanoid();
await Deno.writeTextFile(
path.join(state.stateRoot!, ".wmill", fileName),
val,
{ create: true },
);
state.contentFiles.set(id, fileName);
}
}
}
await state.save();
}
async function pull(
opts: GlobalOptions & { raw: boolean; rawOverride: boolean },
) {
if (opts.raw) {
const opts2 = opts as any;
opts2.override = opts2.rawOverride;
opts2.raw = undefined;
opts2.rawOverride = undefined;
await pullRaw(opts2, Deno.cwd());
return;
}
const state = await getState(opts);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log("Pulling remote changes");
await updateStateFromRemote(workspace, state, (_) => true);
const diffs = diffState(state);
await copyNonJsonFiles(state);
console.log(`Applying changes to files`);
for await (const diff of diffs) {
await applyDiff(
diff.diff,
path.join(state.stateRoot!, diff.localPath),
);
}
console.log(colors.green.underline("Done! All changes applied."));
async function applyDiff(diffs: Difference[], file: string) {
ensureDir(path.dirname(file));
let json;
try {
json = JSON.parse(await Deno.readTextFile(file));
} catch {
json = {};
}
// TODO: Delegate the below to the object itself
// This would work by infering the type of `JSON` (which includes then statically typing it using decoverto) and then
// delegating the applying of the diffs to the object via an interface
for (const diff of diffs) {
if (diff.type === "CREATE") {
setValueByPath(json, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(json, diff.path, undefined);
} else if (diff.type === "CHANGE") {
setValueByPath(json, diff.path, diff.value);
}
}
// TODO: Use decoverto instanceToPlain below.
await Deno.writeTextFile(file, JSON.stringify(json, undefined, " "), {
create: true,
});
}
async function copyNonJsonFiles(state: State): Promise<void> {
for (const t of state.tracked.keys()) {
if (t.endsWith(".json")) {
continue;
}
const entry = state.get(t);
const target = await Deno.open(entry.path, { create: true, write: true });
const source = await Deno.open(
path.join(
state.stateRoot!,
".wmill",
state.contentFile(entry.getId())!,
),
{
read: true,
write: false,
},
);
await source.readable.pipeTo(target.writable);
}
}
}
async function* diffState(state: State): AsyncGenerator<StateDiff, void, void> {
for (const t of state.tracked.keys()) {
if (!t.endsWith(".json")) {
continue;
}
const entry = state.get(t);
let fileText;
try {
fileText = await Deno.readTextFile(
path.join(state.stateRoot!, entry.path),
);
} catch {
fileText = "{}";
}
const old = JSON.parse(fileText);
const fileHash = objectHash(old);
if (fileHash !== entry.getHash()) {
const stateContent = await entry.getContent() as any;
const diff = microdiff(
old,
stateContent ?? {},
{ cyclesFix: false },
);
yield new StateDiff(entry.getId(), entry.path, diff);
}
}
}
function prettyDiff(diffs: Difference[]) {
for (const diff of diffs) {
let pathString = "";
for (const pathSegment of diff.path) {
if (typeof pathSegment === "string") {
pathString += ".";
pathString += pathSegment;
} else {
pathString += "[";
pathString += pathSegment;
pathString += "]";
}
}
if (diff.type === "REMOVE" || diff.type === "CHANGE") {
console.log(colors.red("- " + pathString + " = " + diff.oldValue));
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
console.log(colors.green("+ " + pathString + " = " + diff.value));
}
}
}
async function push(opts: GlobalOptions & { raw: boolean }) {
if (opts.raw) {
const opts2 = opts as any;
opts2.raw = undefined;
await pushRaw(opts2, undefined);
return;
}
const state = await getState(opts);
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
let error = false;
await updateStateFromRemote(workspace, state, async (filename) => {
const e = state.get(filename);
if (!e) {
throw new Error("!? State change on untracked file ?!");
}
let fileJSON;
try {
fileJSON = JSON.parse(
await Deno.readTextFile(path.join(state.stateRoot!, e.path)),
);
} catch {
fileJSON = {};
}
const file = inferTypeFromPath(e.path, fileJSON);
const eContent = (inferTypeFromPath(e.path, await e.getContent())) ?? {};
const fileHash = objectHash(file);
const eHash = objectHash(eContent);
if (fileHash !== eHash) {
console.log(
colors.red("!! Local and Remote change present. Local diff:"),
);
prettyDiff(microdiff(eContent as any, file, { cyclesFix: false }));
console.log(
colors.red(
"Consider comitting or otherwise saving your work and pulling to load any remote changes",
),
);
error = true;
return false;
}
return true;
});
if (error) {
return;
}
for (const p of state.tracked.keys()) {
if (!p.endsWith(".json")) continue;
const entry = state.get(p);
let fileJSON;
try {
fileJSON = JSON.parse(
await Deno.readTextFile(path.join(state.stateRoot!, entry.path)),
);
} catch {
fileJSON = {};
}
const file = inferTypeFromPath(entry.path, fileJSON);
const eContent =
(inferTypeFromPath(entry.path, await entry.getContent())) ?? {};
const fileHash = objectHash(file);
const eHash = objectHash(eContent);
if (fileHash !== eHash) {
const remotePath = entry.path.split(".")[0];
const type = getTypeStrFromPath(entry.path);
if (type === "script") {
// Diffing makes no sense for scripts - instead fetch parent hash & check hash again.
// If hash is still missmatched - create new script as child.
const typed = decoverto.type(ScriptFile).plainToInstance(file);
const contentPath = await findContentFile(entry.path);
const language = inferContentTypeFromFilePath(contentPath);
const content = await Deno.readTextFile(contentPath);
try {
const remote = await ScriptService.getScriptByPath({
workspace: workspace.workspaceId,
path: remotePath,
});
if (objectHash(remote) !== fileHash) {
await ScriptService.createScript({
workspace: workspace.workspaceId,
requestBody: {
content,
description: typed.description,
language,
path: remotePath,
summary: typed.summary,
is_template: typed.is_template,
kind: typed.kind,
lock: undefined,
parent_hash: remote.hash,
schema: typed.schema,
},
});
}
} catch {
// no parent hash
await ScriptService.createScript({
workspace: workspace.workspaceId,
requestBody: {
content,
description: typed.description,
language,
path: remotePath,
summary: typed.summary,
is_template: typed.is_template,
kind: typed.kind,
lock: undefined,
parent_hash: undefined,
schema: typed.schema,
},
});
}
} else {
const diff = microdiff(eContent as any, file, { cyclesFix: false });
await applyDiff(
workspace.workspaceId,
remotePath,
file,
diff,
);
}
}
}
let anyRemoteChanges = false;
await updateStateFromRemote(workspace, state, (_) => {
anyRemoteChanges = true;
return true;
});
if (anyRemoteChanges) {
console.log("New remote changes - consider pulling");
}
function applyDiff(
workspace: string,
remotePath: string,
file:
| ScriptFile
| VariableFile
| FlowFile
| ResourceFile
| ResourceTypeFile
| FolderFile,
diffs: Difference[],
) {
if (file instanceof ScriptFile) {
throw new Error(
"This code path should be unreachable - we should never generate diffs for scripts",
);
} else if (file instanceof FolderFile) {
const parts = remotePath.split("/");
if (parts[0] === "f") {
remotePath = parts[1];
} else {
remotePath = parts[0];
}
}
return file.pushDiffs(workspace, remotePath, diffs);
}
}
class StateDiff {
trackedId: TrackedId;
localPath: string;
diff: Difference[];
constructor(
trackedId: TrackedId,
localPath: string,
diff: Difference[],
) {
this.trackedId = trackedId;
this.localPath = localPath;
this.diff = diff;
}
}
async function add(opts: GlobalOptions, path: string) {
const state = await getState(opts);
// TODO: Automatically check whether this path exists either locally or on the remote
state.add(path);
if (path.endsWith(".script.json")) {
try {
const f = await findContentFile(path);
state.add(f);
} catch {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
try {
const old = await ScriptService.getScriptByPath({
workspace: workspace.workspaceId,
path: path.split(".")[0],
});
if (old.language === "python3") {
state.add(path.replace(".script.json", ".py"));
} else if (old.language === "bash") {
state.add(path.replace(".script.json", ".sh"));
} else if (old.language === "deno") {
state.add(path.replace(".script.json", ".ts"));
} else if (old.language === "go") {
state.add(path.replace(".script.json", ".go"));
} else {
throw new Error("Remote returned invalid language?! " + old.language);
}
} catch {
throw new Error(
"Could not infer script language from local or remote. Exiting.",
);
}
}
}
await state.save();
}
async function init(opts: GlobalOptions) {
const root = Deno.cwd();
try {
await Deno.mkdir(path.join(root, ".wmill"));
} catch {
console.log(
colors.red(
"! Looks like this folder is already initialized or we are missing permissions to do so. Exiting.",
),
);
return;
}
const workspace = await resolveWorkspace(opts);
const newState = new State(
new Map(),
new Map(),
new Map(),
workspace.workspaceId,
workspace.remote,
);
newState.stateRoot = root;
await newState.save();
}
async function pullRaw(
opts: GlobalOptions & { override: boolean },
dir: string,
) {
const workspace = await resolveWorkspace(opts);
const untar = await downloadTar(workspace);
if (!untar) return;
for await (const entry of untar) {
console.log(entry.fileName);
const filePath = path.resolve(dir, entry.fileName);
if (entry.type === "directory") {
await ensureDir(filePath);
continue;
}
await ensureDir(path.dirname(filePath));
if (!opts.override) {
let exists = false;
try {
const _stat = await Deno.stat(filePath);
exists = true;
} catch {
exists = false;
}
if (exists) {
if (
!(await Confirm.prompt(
"Conflict at " +
filePath +
" do you want to override the local version?",
))
) {
continue;
}
}
}
const file = await Deno.open(filePath, { write: true, create: true });
const len = await copy(entry, file);
await file.truncate(len);
file.close();
}
console.log(colors.green("Done. Wrote all files to disk."));
}
type PushRawCandidate = {
path: string;
namespaceKind: "user" | "group" | "folder";
namespaceName: string;
};
type PushRawResourceTypeCandidate = {
path: string;
};
type PushRawFolderCandidate = {
path: string;
namespaceName: string;
};
async function pushRawFindCandidateFiles(
dir: string,
): Promise<
{
normal: PushRawCandidate[];
resourceTypes: PushRawResourceTypeCandidate[];
folders: PushRawFolderCandidate[];
}
> {
dir = path.resolve(dir);
if (path.dirname(dir).startsWith(".")) {
return { normal: [], resourceTypes: [], folders: [] };
}
const normalCandidates: PushRawCandidate[] = [];
const resourceTypeCandidates: PushRawResourceTypeCandidate[] = [];
const folderCandidates: PushRawFolderCandidate[] = [];
for await (const e of Deno.readDir(dir)) {
if (e.isDirectory) {
if (e.name == "u" || e.name == "g" || e.name == "f") { // TODO: Check version for f
const newDir = dir + (dir.endsWith("/") ? "" : "/") + e.name;
for await (const e2 of Deno.readDir(newDir)) {
if (e2.isDirectory) {
if (e2.name.startsWith(".")) continue;
const namespaceName = e2.name;
const stack: string[] = [];
{
const path = newDir + "/" + namespaceName + "/";
stack.push(path);
try {
await Deno.stat(path + "folder.meta.json");
folderCandidates.push({
namespaceName,
path: path + "folder.meta.json",
});
} catch {}
}
while (stack.length > 0) {
const dir2 = stack.pop()!;
for await (const e3 of Deno.readDir(dir2)) {
if (e3.isFile) {
if (e3.name === "folder.meta.json") continue;
normalCandidates.push({
path: dir2 + e3.name,
namespaceKind: e.name == "g"
? "group"
: e.name == "u"
? "user"
: "folder",
namespaceName: namespaceName,
});
} else {
stack.push(dir2 + e3.name + "/");
}
}
}
}
}
} else {
console.log(
colors.yellow(
"Including organizational folder " + e.name + " in push!",
),
);
const { normal, resourceTypes, folders } =
await pushRawFindCandidateFiles(
path.join(dir, e.name),
);
normalCandidates.push(...normal);
resourceTypeCandidates.push(...resourceTypes);
folderCandidates.push(...folders);
}
} else {
// handle root files
if (e.name.endsWith(".resource-type.json")) {
resourceTypeCandidates.push({
path: dir + (dir.endsWith("/") ? "" : "/") + e.name,
});
}
}
}
return {
normal: normalCandidates,
folders: folderCandidates,
resourceTypes: resourceTypeCandidates,
};
}
async function pushRaw(opts: GlobalOptions, dir?: string) {
dir = dir ?? Deno.cwd();
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
console.log(colors.blue("Searching Directory..."));
const { normal, resourceTypes, folders } = await pushRawFindCandidateFiles(
dir,
);
console.log(
colors.blue(
"Found " + (normal.length + resourceTypes.length + folders.length) +
" candidates",
),
);
for (const resourceType of resourceTypes) {
const fileName = resourceType.path.substring(
resourceType.path.lastIndexOf("/") + 1,
);
const fileNameParts = fileName.split(".");
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + resourceType.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + resourceType.path));
continue;
}
console.log("pushing resource type " + fileNameParts.at(-3)!);
await decoverto.type(ResourceTypeFile).rawToInstance(
await Deno.readTextFile(resourceType.path),
).push(workspace.workspaceId, fileNameParts.at(-3)!);
}
for (const folder of folders) {
await decoverto.type(FolderFile).plainToInstance(
JSON.parse(await Deno.readTextFile(folder.path)),
).push(
workspace.workspaceId,
"f/" + folder.namespaceName,
);
}
for (const candidate of normal) {
// full file name. No leading /. includes .type.json
const fileName = candidate.path.substring(
candidate.path.lastIndexOf("/") + 1,
);
// figure out just the path after ...../u|g/username|group/ (in extra dir)
const dirParts = candidate.path.split("/").filter((x) => x.length > 0);
// TODO: check version for folder
const gIndex = dirParts.findIndex((x) => x == "u" || x == "g" || x == "f");
const extraDir = dirParts.slice(gIndex + 2, -1).join("/");
// file name parts has .json (hopefully) at -1, type at -2, and the actual name at -3. Dots in names are not allowed.
const fileNameParts = fileName.split(".");
// filter out script content files
if (
fileNameParts.at(-1) == "ts" ||
fileNameParts.at(-1) == "py" ||
fileNameParts.at(-1) == "go"
) {
// probably part of a script. Silent ignore.
continue;
}
// invalid file names, like my.cool.script.script.json. Not valid.
if (fileNameParts.length != 3) {
console.log(
colors.yellow("invalid file name found at " + candidate.path),
);
continue;
}
// filter out non-json files. Note that we filter out script contents above, so this is really an error.
if (fileNameParts.at(-1) != "json") {
console.log(colors.yellow("non-JSON file found at " + candidate.path));
continue;
}
// get the type & filter it for valid ones.
const type = fileNameParts.at(-2);
if (type == "resource-type") {
console.log(
colors.yellow(
"Found resource type file at " +
candidate.path +
" this appears to be inside a path folder. Resource types are not addressed by path. Place them at the root or inside only an organizational folder. Ignoring this file!",
),
);
continue;
}
if (
type != "flow" &&
type != "resource" &&
type != "script" &&
type != "variable"
) {
console.log(
colors.yellow(
"file with invalid type " + type + " found at " + candidate.path,
),
);
continue;
}
// create the remotePath for the API
const remotePath = (candidate.namespaceKind === "group"
? "g/"
: (candidate.namespaceKind === "user" ? "u/" : "f/")) +
candidate.namespaceName +
"/" +
(extraDir.length > 0 ? extraDir + "/" : "") +
fileNameParts.at(-3);
console.log("pushing " + type + " to " + remotePath);
const typed = inferTypeFromPath(
candidate.path,
JSON.parse(await Deno.readTextFile(candidate.path)),
);
if (typed instanceof ResourceTypeFile || typed instanceof FolderFile) {
throw new Error(
"Resource Types and Folders should be filtered out at this point!",
);
} else if (typed instanceof ScriptFile) {
let contentPath: string;
try {
contentPath = await findContentFile(candidate.path);
} catch (e) {
console.log(colors.red(e.toString()));
continue;
}
await pushScript(
candidate.path,
contentPath,
workspace.workspaceId,
remotePath,
);
} else {
typed.push(workspace.workspaceId, remotePath);
}
}
console.log(colors.underline.bold.green("Successfully Pushed all files."));
}
const command = new Command()
.command("init")
.description(
"Initialize this folder as sync root for the currently selected workspace & remote.",
)
.action(init as any)
.command("add")
.description("Add a local file for tracking")
.arguments("<path:string>")
.action(add as any)
.command("pull")
.description("Pull any remote changes and apply them locally")
.option("--raw", "Pull without using state.")
.option("--raw-override", "Always override local files with remote.", {
depends: ["raw"],
})
.action(pull as any)
.command("push")
.description("Push any local changes and apply them remotely")
.option("--raw", "Push without using state.")
.action(push as any);
export default command;

View File

@@ -0,0 +1,10 @@
{
"workspace_id": "admins",
"name": "my_folder",
"display_name": "my_folder",
"owners": [],
"extra_perms": {
"u/test": true,
"u/admin@windmill.dev": false
}
}

View File

@@ -0,0 +1,12 @@
{
"summary": "Syncronize Hub Resource types with starter workspace",
"description": "Basic administrative script to sync latest resource types from hub. Recommended to run at least once. On a schedule by default.",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {},
"required": [],
"type": "object"
},
"is_template": false,
"lock": []
}

View File

@@ -0,0 +1,13 @@
import wmill from "https://deno.land/x/wmill@v1.55.0/main.ts";
export async function main() {
await run(
"workspace", "add", "__automation", "starter", Deno.env.get("WM_BASE_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
await run("hub", "pull");
}
async function run(...cmd: string[]) {
console.log("Running \"" + cmd.join(' ') + "\"");
await wmill.parse(cmd);
}

View File

@@ -1,137 +1,4 @@
import { decoverto } from "./decoverto.ts";
import { FlowFile } from "./flow.ts";
import { ResourceTypeFile } from "./resource-type.ts";
import { ResourceFile } from "./resource.ts";
import { ScriptFile } from "./script.ts";
import { VariableFile } from "./variable.ts";
import { path } from "./deps.ts";
import { FolderFile } from "./folder.ts";
// TODO: Remove this & replace with a "pull" that lets the object either pull the remote version or return undefined.
// Then combine those with diffing, which then gives the new push impl
export interface Resource {
push(workspace: string, remotePath: string): Promise<void>;
}
export interface PushDiffs {
pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void>;
}
export interface DifferenceCreate {
type: "CREATE";
path: (string | number)[];
value: any;
}
export interface DifferenceRemove {
type: "REMOVE";
path: (string | number)[];
oldValue: any;
}
export interface DifferenceChange {
type: "CHANGE";
path: (string | number)[];
value: any;
oldValue: any;
}
export type Difference = DifferenceCreate | DifferenceRemove | DifferenceChange;
export function setValueByPath(
obj: any,
path: (string | number)[],
value: any,
) {
let i;
let lastObj = undefined;
for (i = 0; i < path.length - 1; i++) {
if (!obj) {
let oldNewObj;
if (typeof path[i] === "number") {
oldNewObj = [];
} else {
oldNewObj = {};
}
lastObj[path[i - 1]] = oldNewObj;
obj = oldNewObj;
}
lastObj = obj;
obj = obj[path[i]];
}
if (!obj) {
let oldNewObj;
if (typeof path[i] === "number") {
oldNewObj = [];
} else {
oldNewObj = {};
}
lastObj[path[i - 1]] = oldNewObj;
obj = oldNewObj;
}
obj[path[i]] = value;
}
export type GlobalOptions = {
workspace: string | undefined;
token: string | undefined;
};
export function inferTypeFromPath(
p: string,
obj: any,
):
| ScriptFile
| VariableFile
| FlowFile
| ResourceFile
| ResourceTypeFile
| FolderFile {
const typeEnding = getTypeStrFromPath(p);
if (typeEnding === "folder") {
return decoverto.type(FolderFile).plainToInstance(obj);
} else if (typeEnding === "script") {
return decoverto.type(ScriptFile).plainToInstance(obj);
} else if (typeEnding === "variable") {
return decoverto.type(VariableFile).plainToInstance(obj);
} else if (typeEnding === "flow") {
return decoverto.type(FlowFile).plainToInstance(obj);
} else if (typeEnding === "resource") {
return decoverto.type(ResourceFile).plainToInstance(obj);
} else if (typeEnding === "resource-type") {
return decoverto.type(ResourceTypeFile).plainToInstance(obj);
} else {
throw new Error("infer type unreachable");
}
}
export function getTypeStrFromPath(
p: string,
): "script" | "variable" | "flow" | "resource" | "resource-type" | "folder" {
const parsed = path.parse(p);
if (parsed.ext !== ".json") {
throw new Error(
"Cannot infer type of non-json file " + JSON.stringify(parsed),
);
}
if (parsed.name === "folder.meta") {
return "folder";
}
const typeEnding = parsed.name.split(".").at(-1);
if (
typeEnding === "script" || typeEnding === "variable" ||
typeEnding === "flow" || typeEnding === "resource" ||
typeEnding === "resource-type"
) {
return typeEnding;
} else {
throw new Error("Could not infer type of path " + JSON.stringify(parsed));
}
}

View File

@@ -1,22 +1,7 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import {
Difference,
GlobalOptions,
PushDiffs,
Resource,
setValueByPath,
} from "./types.ts";
import {
colors,
Command,
EditVariable,
ListableVariable,
microdiff,
Table,
VariableService,
} from "./deps.ts";
import { decoverto, model, property } from "./decoverto.ts";
import { GlobalOptions } from "./types.ts";
import { colors, Command, Table, VariableService } from "./deps.ts";
async function list(opts: GlobalOptions) {
const workspace = await resolveWorkspace(opts);
@@ -41,100 +26,13 @@ async function list(opts: GlobalOptions) {
.render();
}
@model()
export class VariableFile implements Resource, PushDiffs {
@property(() => String)
type VariableFile = {
value: string;
@property(() => Boolean)
is_secret: boolean;
@property(() => String)
description: string;
@property(() => Number)
account?: number;
@property(() => Boolean)
is_oauth?: boolean;
constructor(value: string, is_secret: boolean, description: string) {
this.value = value;
this.is_secret = is_secret;
this.description = description;
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
): Promise<void> {
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing variable...`,
),
);
const changeset: EditVariable = {};
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["path", "value", "is_secret", "description"].includes(
diff.path[0] as string,
)
)
) {
throw new Error("Invalid variable diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
setValueByPath(changeset, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(changeset, diff.path, null);
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
console.log(colors.yellow("! Skipping empty changeset"));
return;
}
await VariableService.updateVariable({
workspace,
path: remotePath,
requestBody: changeset,
});
} else {
console.log(colors.yellow("Creating new variable..."));
await VariableService.createVariable({
workspace,
requestBody: {
path: remotePath,
description: this.description,
is_secret: this.is_secret,
value: this.value,
account: this.account,
is_oauth: this.is_oauth,
},
});
}
}
async push(workspace: string, remotePath: string): Promise<void> {
let existing: ListableVariable | undefined;
try {
existing = await VariableService.getVariable({
workspace: workspace,
path: remotePath,
});
} catch {
existing = undefined;
}
await this.pushDiffs(
workspace,
remotePath,
microdiff(existing ?? {}, this, { cyclesFix: false }),
);
}
}
};
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
@@ -160,10 +58,72 @@ export async function pushVariable(
filePath: string,
remotePath: string,
) {
const data = decoverto.type(VariableFile).rawToInstance(
await Deno.readTextFile(filePath),
);
await data.push(workspace, remotePath);
const data: VariableFile = JSON.parse(await Deno.readTextFile(filePath));
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
const existing = await VariableService.getVariable({
workspace: workspace,
path: remotePath,
});
if (existing.is_oauth != data.is_oauth) {
console.log(
colors.red.underline.bold(
"Remote variable at " +
remotePath +
" exists & has a different oauth state. This cannot be updated. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
if (existing.account != data.account) {
console.log(
colors.red.underline.bold(
"Remote variable at " +
remotePath +
" exists & has a different account state. This cannot be updated. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
if (existing.is_secret && !data.is_secret) {
console.log(
colors.red.underline.bold(
"Remote variable at " +
remotePath +
" exists & is secret. Variables cannot be updated to be no longer secret. If you wish to do this anyways, consider deleting the remote resource.",
),
);
return;
}
const actual_secret = data.is_secret ? true : undefined;
console.log(colors.yellow("Updating existing variable..."));
await VariableService.updateVariable({
workspace,
path: remotePath,
requestBody: {
description: data.description,
is_secret: actual_secret,
path: remotePath,
value: data.value,
},
});
} else {
console.log(colors.yellow("Creating new variable..."));
await VariableService.createVariable({
workspace,
requestBody: {
path: remotePath,
description: data.description,
is_secret: data.is_secret,
value: data.value,
account: data.account,
is_oauth: data.is_oauth,
},
});
}
}
const command = new Command()

View File

@@ -11,31 +11,14 @@ import {
Table,
WorkspaceService,
} from "./deps.ts";
import { decoverto, model, property } from "./decoverto.ts";
import { requireLogin } from "./context.ts";
@model()
export class Workspace {
@property(() => String)
export type Workspace = {
remote: string;
@property(() => String)
workspaceId: string;
@property(() => String)
name: string;
@property(() => String)
token: string;
constructor(
remote: string,
workspaceId: string,
name: string,
token: string,
) {
this.remote = remote;
this.workspaceId = workspaceId;
this.name = name;
this.token = token;
}
}
};
function makeWorkspaceStream(
readable: ReadableStream<Uint8Array>,
@@ -50,9 +33,7 @@ function makeWorkspaceStream(
if (line.length <= 2) {
return;
}
const workspace = decoverto.type(Workspace).rawToInstance(line);
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
controller.enqueue(workspace);
controller.enqueue(JSON.parse(line) as Workspace);
} catch {
/* ignore */
}
@@ -61,17 +42,14 @@ function makeWorkspaceStream(
);
}
export async function getWorkspaceStream() {
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
write: false,
read: true,
});
return makeWorkspaceStream(file.readable);
}
export async function allWorkspaces(): Promise<Workspace[]> {
async function allWorkspaces(): Promise<Workspace[]> {
try {
const workspaceStream = await getWorkspaceStream();
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
write: false,
read: true,
});
const workspaceStream = makeWorkspaceStream(file.readable);
const workspaces: Workspace[] = [];
for await (const workspace of workspaceStream) {
workspaces.push(workspace);
@@ -109,7 +87,8 @@ export async function getActiveWorkspace(
export async function getWorkspaceByName(
workspaceName: string,
): Promise<Workspace | undefined> {
const workspaceStream = await getWorkspaceStream();
const file = await Deno.open((await getRootStore()) + "remotes.ndjson");
const workspaceStream = makeWorkspaceStream(file.readable);
for await (const workspace of workspaceStream) {
if (workspace.name === workspaceName) {
return workspace;
@@ -206,7 +185,6 @@ export async function add(
remote = new URL(await Input.prompt("Enter the Remote URL")).toString();
}
}
remote = new URL(remote).toString(); // add trailing slash in all cases!
let token = await tryGetLoginInfo(opts);
while (!token) {
@@ -214,10 +192,7 @@ export async function add(
}
if (opts.create) {
setClient(
token,
remote.endsWith("/") ? remote.substring(0, remote.length - 1) : remote,
);
setClient(token, remote.endsWith('/') ? remote.substring(0, remote.length - 1) : remote);
if (
!await WorkspaceService.existsWorkspace({
@@ -249,7 +224,6 @@ export async function add(
}
export async function addWorkspace(workspace: Workspace) {
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
append: true,
write: true,
@@ -260,7 +234,7 @@ export async function addWorkspace(workspace: Workspace) {
file.close();
}
export async function removeWorkspace(name: string) {
async function remove(_opts: GlobalOptions, name: string) {
const orgWorkspaces = await allWorkspaces();
await Deno.writeTextFile(
(await getRootStore()) + "remotes.ndjson",
@@ -269,10 +243,6 @@ export async function removeWorkspace(name: string) {
.map((x) => JSON.stringify(x))
.join("\n"),
);
}
async function remove(_opts: GlobalOptions, name: string) {
await removeWorkspace(name);
console.log(colors.green.underline("Succesfully removed workspace!"));
}

View File

@@ -1,12 +1,12 @@
{
"name": "windmill",
"version": "1.62.0",
"version": "1.61.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.62.0",
"version": "1.61.1",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
@@ -65,7 +65,7 @@
"svelte-highlight": "^6.2.1",
"svelte-overlay": "^1.4.1",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.0",
"svelte-splitpanes": "^0.7.3",
"svelte2tsx": "^0.5.22",
"tailwindcss": "^3.2.2",
@@ -5832,18 +5832,18 @@
}
},
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
"integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^0.2.5",
"minimist": "^1.2.0",
"sander": "^0.5.0"
"sander": "^0.5.0",
"sourcemap-codec": "^1.3.0"
},
"bin": {
"sorcery": "bin/sorcery"
"sorcery": "bin/index.js"
}
},
"node_modules/source-map": {
@@ -5864,6 +5864,12 @@
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"node_modules/spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -6272,9 +6278,9 @@
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
},
"node_modules/svelte-preprocess": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.1.tgz",
"integrity": "sha512-0HXyhCoc9rsW4zGOgtInylC6qj259E1hpFnJMJWTf+aIfeqh4O/QHT31KT2hvPEqQfdjmqBR/kO2JDkkciBLrQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
"integrity": "sha512-q7lpa7i2FBu8Pa+G0MmuQQWETBwCKgsGmuq1Sf6n8q4uaG9ZLcLP0Y+etC6bF4sE6EbLxfiI38zV6RfPe3RSfg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -6282,7 +6288,7 @@
"@types/sass": "^1.43.1",
"detect-indent": "^6.1.0",
"magic-string": "^0.27.0",
"sorcery": "^0.11.0",
"sorcery": "^0.10.0",
"strip-indent": "^3.0.0"
},
"engines": {
@@ -11225,15 +11231,15 @@
}
},
"sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
"integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
"integrity": "sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==",
"dev": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.4.14",
"buffer-crc32": "^0.2.5",
"minimist": "^1.2.0",
"sander": "^0.5.0"
"sander": "^0.5.0",
"sourcemap-codec": "^1.3.0"
}
},
"source-map": {
@@ -11248,6 +11254,12 @@
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -11575,16 +11587,16 @@
"integrity": "sha512-jhtZWtD6cUE2nMw46dJ5VXWYiqnER+JH+V/BmNBQ5fNP/YdsJCpJi+DemUy9msklqGb0f+wLhJPBtKHLWQvzjg=="
},
"svelte-preprocess": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.1.tgz",
"integrity": "sha512-0HXyhCoc9rsW4zGOgtInylC6qj259E1hpFnJMJWTf+aIfeqh4O/QHT31KT2hvPEqQfdjmqBR/kO2JDkkciBLrQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.0.tgz",
"integrity": "sha512-q7lpa7i2FBu8Pa+G0MmuQQWETBwCKgsGmuq1Sf6n8q4uaG9ZLcLP0Y+etC6bF4sE6EbLxfiI38zV6RfPe3RSfg==",
"dev": true,
"requires": {
"@types/pug": "^2.0.6",
"@types/sass": "^1.43.1",
"detect-indent": "^6.1.0",
"magic-string": "^0.27.0",
"sorcery": "^0.11.0",
"sorcery": "^0.10.0",
"strip-indent": "^3.0.0"
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.62.0",
"version": "1.61.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -51,7 +51,7 @@
"svelte-highlight": "^6.2.1",
"svelte-overlay": "^1.4.1",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
"svelte-preprocess": "^5.0.0",
"svelte-splitpanes": "^0.7.3",
"svelte2tsx": "^0.5.22",
"tailwindcss": "^3.2.2",

View File

@@ -1,51 +0,0 @@
<script lang="ts">
import { page } from '$app/stores'
import { userWorkspaces, workspaceStore } from '$lib/stores'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
import InlineCodeCopy from './InlineCodeCopy.svelte'
$: opened = false
$: workspace = $userWorkspaces.find((e) => e.id === $workspaceStore)
$: workspaceName = workspace?.name
$: workspaceId = workspace?.id
$: url = `${$page.url.protocol}//${$page.url.hostname}/`
</script>
<div class="text-sm mt-2 flex">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="underline mr-4"
on:click={() => {
opened = !opened
}}
>
CLI quick setup
<Icon class="ml-2" data={opened ? faChevronUp : faChevronDown} scale={0.7} />
</span>
</div>
{#if opened}
<div
transition:slide|local
class="bg-gray-100 border-l-4 border-gray-600 text-gray-700 p-4 m-4"
role="alert"
id="dynamic-input-help-box"
>
<ul class="pl-0 list-decimal list-inside">
<li
>Install the latest wmill CLI from deno.land: <InlineCodeCopy
content={'deno install --unstable -A https://deno.land/x/wmill/main.ts'}
/></li
>
<li
>Setup the wmill cli for this workspace & remote: <InlineCodeCopy
content={`wmill workspace add ${workspaceName} ${workspaceId} ${url}`}
/></li
>
<li>Follow the prompts in your terminal</li>
<li>Use the run command above!</li>
</ul>
</div>
{/if}

View File

@@ -29,8 +29,6 @@
import PickHubScript from './flows/pickers/PickHubScript.svelte'
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Popover from './Popover.svelte'
import Kbd from './common/kbd/Kbd.svelte'
export let lang: 'python3' | 'deno' | 'go' | 'bash'
export let editor: Editor
@@ -227,137 +225,121 @@
<ResourceEditor bind:this={resourceEditor} on:refresh={resourcePicker.openDrawer} />
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
<div class="flex justify-between items-center overflow-y-auto w-full p-1">
<div class="flex items-center">
<Badge color={validCode ? 'green' : 'red'} class="min-w-[60px] mr-3">
{validCode ? 'Valid' : 'Invalid'}
</Badge>
<div class="flex items-center divide-x">
<Popover notClickable placement="bottom" disapperTimoout={0} class="pr-1" disablePopup={!iconOnly}>
<Button
color="light"
btnClasses="!font-medium !h-full"
on:click={contextualVariablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Context Var
</Button>
<svelte:fragment slot="text">
Add context variable
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
color="light"
btnClasses="!font-medium !h-full"
on:click={variablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Variable
</Button>
<svelte:fragment slot="text">
Add variable
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCube }}
>
+Resource
</Button>
<svelte:fragment slot="text">
Add resource
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset
</Button>
<svelte:fragment slot="text">
Reset
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor.reloadWebsocket}
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
Assistant
<div class="flex flex-row justify-between items-center overflow-hidden w-full px-1">
<div class="flex flex-row divide-x items-center">
<div class="mx-2">
{#if validCode}
<Badge color="green">Inputs</Badge>
{:else}
<Badge color="red">Inputs</Badge>
{/if}
</div>
<div>
<Button
color="light"
btnClasses="mr-1 !font-medium"
on:click={contextualVariablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Context Var
</Button>
</div>
<div>
<Button
color="light"
btnClasses="mx-1 !font-medium"
on:click={variablePicker.openDrawer}
size="xs"
spacingSize="md"
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Variable
</Button>
</div>
<div>
<Button
btnClasses="mx-1 !font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={resourcePicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCube }}
>
+Resource
</Button>
</div>
<div>
<Button
btnClasses="mx-1 !font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset
</Button>
</div>
<div>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.reloadWebsocket}
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
Assistant
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.black ? 'green' : 'text-red-700'}>Black</span>)
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
(<span class={websocketAlive.deno ? 'green' : 'text-red-700'}>Deno</span>)
{:else if lang == 'go'}
(<span class={websocketAlive.go ? 'green' : 'text-red-700'}>Go</span>)
{:else if lang == 'python3'}
(<span class={websocketAlive.pyright ? 'green' : 'text-red-700'}>Pyright</span>
<span class={websocketAlive.black ? 'green' : 'text-red-700'}>Black</span>)
{/if}
</span>
</Button>
<svelte:fragment slot="text">
Reload assistant
</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.format}
{iconOnly}
startIcon={{ icon: faBroom }}
>
</span>
</Button></div
>
<div>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.format}
startIcon={{ icon: faBroom }}
>
{#if !iconOnly}
Format (Ctrl+S)
</Button>
<svelte:fragment slot="text">
Format <Kbd class="!text-gray-800">Ctrl</Kbd> + <Kbd class="!text-gray-800">S</Kbd>
</svelte:fragment>
</Popover>
{/if}
</Button></div
>
</div>
<div class="py-1">
<div>
<Button
btnClasses="mx-1 !font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faEye }}
>
Script
</Button>
</div>
</div>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faEye }}
>
Script
</Button>
<svelte:fragment slot="text">
Script
</svelte:fragment>
</Popover>
</div>
<style>

View File

@@ -1,13 +0,0 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
import { Icon } from 'svelte-awesome'
export let content: string
</script>
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap"
>{content} <span on:click={() => copyToClipboard(content)} class="cursor-pointer ml-2"
><Icon data={faClipboard} /></span
></pre
>

View File

@@ -5,7 +5,6 @@
export let placement: PopoverPlacement = 'auto'
export let notClickable = false
export let popupClass = ''
export let disablePopup = false
export let disapperTimoout = 100
const [popperRef, popperContent] = createPopperActions({ placement })
@@ -21,12 +20,7 @@
const extraOpts = {
modifiers: [
betterPreventOverflow({ padding: 10 }),
{
name: 'offset',
options: {
offset: [8, 8]
}
},
{ name: 'offset', options: { offset: [8, 8] } },
{
name: 'arrow',
options: {
@@ -68,7 +62,7 @@
<slot />
</button>
{/if}
{#if showTooltip && !disablePopup}
{#if showTooltip}
<div
use:popperContent={extraOpts}
on:mouseenter={open}

View File

@@ -18,8 +18,6 @@
import Toggle from './Toggle.svelte'
import { userStore } from '$lib/stores'
import Tooltip from './Tooltip.svelte'
import CliHelpBox from './CliHelpBox.svelte'
import InlineCodeCopy from './InlineCodeCopy.svelte'
export let runnable:
| {
@@ -57,15 +55,10 @@
export let isValid = true
// Run later
let viewOptions = false
let viewCliOptions = false
let scheduledForStr: string | undefined
let invisible_to_owner: false
$: cliCommand = `wmill ${runnable?.kind} run ${runnable?.path} ${Object.entries(args)
.map(([k, v]) => `-i ${k}=${JSON.stringify(v)}`)
.join(' ')}`
</script>
<div class="max-w-6xl">
@@ -178,7 +171,7 @@
<div class="flex items-center gap-1">
<Toggle
options={{
right: `make run invisible to others`
right: `run only visible to you`
}}
bind:checked={invisible_to_owner}
/>
@@ -207,20 +200,4 @@
{buttonText}
</Button>
{/if}
<div class="my-10" />
<Button
color="light"
size="sm"
endIcon={{ icon: viewCliOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewCliOptions = !viewCliOptions)}
>
Run it from the CLI
</Button>
{#if viewCliOptions}
<div transition:slide class="mt-2 px-4 pt-2">
<InlineCodeCopy content={cliCommand} />
<CliHelpBox />
</div>
{/if}
</div>

View File

@@ -84,7 +84,6 @@
}
})
sendUserToast(`Success! New script version created with hash ${newHash}`)
await goto(`/scripts/edit/${newHash}?step=2`)
goto(`/scripts/get/${newHash}`)
} catch (error) {
sendUserToast(`Impossible to save the script: ${error.body}`, true)
@@ -302,12 +301,12 @@
/>
</div>
{/if}
<!-- <div class="ml-3">
<div class="ml-3">
<Toggle
bind:checked={script.is_template}
options={{ right: 'Save as a workspace template' }}
/>
</div> -->
</div>
</CenteredPage>
{:else if step === 2}
<ScriptEditor

View File

@@ -109,10 +109,12 @@
console.error(err)
}
}
console.debug('cancelled')
}
export async function clearCurrentJob() {
if (currentId) {
console.debug('clear')
job = undefined
await cancelJob()
}
@@ -178,6 +180,7 @@
async function syncer(id: string): Promise<void> {
if (currentId != id) {
console.debug('stop')
return
}
syncIteration++

View File

@@ -7,7 +7,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
let result: any = undefined
@@ -15,7 +14,7 @@
export const staticOutputs: string[] = ['result', 'loading']
</script>
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap bind:result bind:componentInput {id}>
<div class="w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm">
Results
</div>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { Bar, Line } from 'svelte-chartjs'
import {
Chart as ChartJS,
Title,
@@ -11,6 +12,7 @@
CategoryScale,
BarElement
} from 'chart.js'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
@@ -18,7 +20,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['loading', 'result']
@@ -70,7 +71,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.line} bind:value={lineChart} />
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
{#if result}
{#if lineChart}
<Line {data} {options} />

View File

@@ -4,7 +4,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['result', 'loading']
@@ -21,16 +20,14 @@
bind:clientHeight={h}
bind:clientWidth={w}
>
<RunnableWrapper autoRefresh flexWrap bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper autoRefresh flexWrap bind:componentInput {id} bind:result>
{#key result}
<iframe
frameborder="0"
style="height: {h}px; width: {w}px"
class="p-0"
title="sandbox"
srcdoc={result
? '<scr' + `ipt type="application/javascript" src="/tailwind.js"></script>` + result
: 'No html'}
srcdoc={result ? '<scr' + `ipt src="/tailwind.css"></script>` + result : 'No html'}
/>
{/key}
</RunnableWrapper>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { Doughnut, Pie } from 'svelte-chartjs'
import {
Chart as ChartJS,
Title,
@@ -18,7 +19,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['loading', 'result']
@@ -65,7 +65,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.doughnutStyle} bind:value={doughnut} />
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
{#if result}
{#if doughnut}
<Doughnut {data} {options} />

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import zoomPlugin from 'chartjs-plugin-zoom'
import {
Chart as ChartJS,
Title,
@@ -11,6 +12,7 @@
CategoryScale,
BarElement
} from 'chart.js'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import Scatter from 'svelte-chartjs/Scatter.svelte'
@@ -19,7 +21,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
let zoomable = false
let pannable = false
@@ -69,7 +70,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
{#if result}
<Scatter {data} {options} />
{/if}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { Clipboard } from 'lucide-svelte'
import { copyToClipboard } from '../../../../utils'
import { copyToClipboard } from '../../../../utils'
import Button from '../../../common/button/Button.svelte'
import Popover from '../../../Popover.svelte'
import type { AppInput } from '../../inputType'
@@ -13,7 +13,6 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = 'left'
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['result', 'loading']
@@ -60,7 +59,7 @@
<InputValue {id} input={configuration.style} bind:value={style} />
<InputValue {id} input={configuration.copyButton} bind:value={copyButton} />
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if !result || result === ''}
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
@@ -68,15 +67,25 @@
</div>
{:else}
<div class="flex flex-wrap gap-2">
<svelte:element this={component} class="whitespace-pre-wrap {classes}" style={extraStyle}>
<svelte:element
this={component}
class="whitespace-pre-wrap {classes}"
style={extraStyle}
>
{String(result)}
</svelte:element>
{#if copyButton && result}
<Popover notClickable>
<Button size="xs" btnClasses="!px-2" on:click={() => copyToClipboard(result)}>
<Button
size="xs"
btnClasses="!px-2"
on:click={() => copyToClipboard(result)}
>
<Clipboard size={14} strokeWidth={2} />
</Button>
<svelte:fragment slot="text">Copy to clipboard</svelte:fragment>
<svelte:fragment slot="text">
Copy to clipboard
</svelte:fragment>
</Popover>
{/if}
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import zoomPlugin from 'chartjs-plugin-zoom'
import 'chartjs-adapter-date-fns'
import {
Chart as ChartJS,
Title,
@@ -14,6 +15,7 @@
TimeScale,
LogarithmicScale
} from 'chart.js'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import Scatter from 'svelte-chartjs/Scatter.svelte'
@@ -22,7 +24,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['loading', 'result']
@@ -84,7 +85,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
{#if result}
<Scatter {data} {options} />
{/if}

View File

@@ -2,12 +2,12 @@
import { Loader2 } from 'lucide-svelte'
import { onMount } from 'svelte'
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['result', 'loading']
@@ -41,7 +41,7 @@
</script>
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
{#if !Plotly}
<div class="p-2">
<Loader2 class="animate-spin" />

View File

@@ -8,7 +8,6 @@
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = ['result', 'loading']
@@ -53,7 +52,7 @@
<InputValue {id} input={configuration.canvas} bind:value={canvas} />
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
{#if !vegaEmbed}
<div class="p-2">
<Loader2 class="animate-spin" />

View File

@@ -24,7 +24,6 @@
export let defaultUserInput = false
export let flexWrap = false
export let wrapperClass = ''
export let initializing: boolean | undefined = undefined
const {
worldStore,
@@ -306,7 +305,7 @@
/>
<div class="h-full flex relative flex-row flex-wrap {wrapperClass}">
{#if !initializing && autoRefresh === true}
{#if autoRefresh === true}
<div class="flex absolute top-1 right-1">
<RefreshButton componentId={id} />
</div>

View File

@@ -9,7 +9,6 @@
export let componentInput: AppInput | undefined
export let id: string
export let result: any = undefined
export let initializing: boolean = true
export let extraQueryParams: Record<string, any> = {}
export let autoRefresh: boolean = true
@@ -21,10 +20,6 @@
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
$: if (initializing && result) {
initializing = false
}
if (noBackend && componentInput?.type == 'runnable') {
result = componentInput?.['value']
}
@@ -51,7 +46,6 @@
{id}
{extraQueryParams}
{forceSchemaDisplay}
{initializing}
wrapperClass={runnableClass}
>
<slot />

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext, onMount } from 'svelte'
import type { Output } from '../../rx'
import type { AppEditorContext, BaseAppComponent } from '../../types'
import type { AppEditorContext, BaseAppComponent, ButtonComponent } from '../../types'
import InputValue from '../helpers/InputValue.svelte'
import type { AppInput } from '../../inputType'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
@@ -13,13 +13,11 @@
import AppTableFooter from './AppTableFooter.svelte'
import { tableOptions } from './tableOptions'
import Alert from '$lib/components/common/alert/Alert.svelte'
import type { ButtonComponent } from '../../editor/Component.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: Record<string, AppInput>
export let actionButtons: (BaseAppComponent & ButtonComponent)[]
export let initializing: boolean | undefined = undefined
export const staticOutputs: string[] = [
'selectedRow',
@@ -137,7 +135,7 @@
<InputValue {id} input={configuration.search} bind:value={search} />
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full">
{#if search !== 'Disabled'}
@@ -212,18 +210,21 @@
{/each}
{#if actionButtons.length > 0}
<td class="p-2 " on:click={() => toggleRow(row, rowIndex)}>
<div class="center-center h-full w-full flex-wrap gap-1">
{#each actionButtons as actionButton, actionIndex (actionIndex)}
<AppButton
noWFull
{...actionButton}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
{/each}
</div>
<td
class="p-2 "
on:click={() => toggleRow(row, rowIndex)}
>
<div class="center-center h-full w-full flex-wrap gap-1">
{#each actionButtons as actionButton, actionIndex (actionIndex)}
<AppButton
noWFull
{...actionButton}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
{/each}
</div>
</td>
{/if}
</tr>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { components, type AppComponent } from './Component.svelte'
import type { AppComponent } from '../types'
import { displayData } from '../utils'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
export let component: AppComponent
@@ -10,7 +11,7 @@
<div class="mb-8">
<div class="flex justify-between mb-4">
<span class="text-sm font-bold">{component.id}</span>
<span class="text-sm font-bold">{components[component.type].name}</span>
<span class="text-sm font-bold">{displayData[component.type].name}</span>
</div>
{#if resourceOnly && Object.keys(component.componentInput.fields).filter((fieldKey) => {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { ConnectedAppInput, RowAppInput, StaticAppInput, UserAppInput } from '../inputType'
import type { AppComponent } from './Component.svelte'
import type { AppComponent } from '../types'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>

View File

@@ -186,7 +186,7 @@
<span>Settings</span>
</div>
</Tab>
<div slot="content" class="h-full overflow-y-auto pb-4">
<svelte:fragment slot="content">
<TabContent class="overflow-auto" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
@@ -197,7 +197,7 @@
<TabContent value="insert">
<ComponentList />
</TabContent>
</div>
</svelte:fragment>
</Tabs>
{#if $connectingInput.opened}
<div

View File

@@ -49,11 +49,10 @@
StaticAppInput,
UserAppInput
} from '../inputType'
import type { AppEditorContext } from '../types'
import type { AppComponent, AppEditorContext } from '../types'
import { toStatic } from '../utils'
import AppExportButton from './AppExportButton.svelte'
import AppInputs from './AppInputs.svelte'
import type { AppComponent } from './Component.svelte'
import PanelSection from './settingsPanel/common/PanelSection.svelte'
async function hash(message) {

View File

@@ -75,9 +75,9 @@
$: lockedClasses = isLocked ? '!max-h-[400px] overflow-hidden pointer-events-none' : ''
</script>
<div class="relative h-[calc(100vh-41px)] overflow-auto">
<div class="relative">
<div
class="{$$props.class} {lockedClasses} h-full
class="{$$props.class} {lockedClasses} h-full max-h-[calc(100%-41px)] overflow-auto
w-full {app.fullscreen ? '' : 'max-w-6xl'} mx-auto"
>
{#if $appStore.grid}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import BarChartComponent from '../components/dataDisplay/AppBarChart.svelte'
import DisplayComponent from '../components/DisplayComponent.svelte'
import TableComponent from '../components/table/AppTable.svelte'
import TextComponent from '../components/dataDisplay/AppText.svelte'
import type { AppComponent, AppEditorContext } from '../types'
import ButtonComponent from '../components/buttons/AppButton.svelte'
import PieChartComponent from '../components/dataDisplay/AppPieChart.svelte'
import SelectComponent from '../components/selectInputs/AppSelect.svelte'
import CheckboxComponent from '../components/selectInputs/AppCheckbox.svelte'
import TextInputComponent from '../components/textInputs/AppTextInput.svelte'
import NumberInputComponent from '../components/numberInputs/AppNumberInput.svelte'
import DateInputComponent from '../components/dateInputs/AppDateInput.svelte'
import ComponentHeader from './ComponentHeader.svelte'
import AppForm from '../components/form/AppForm.svelte'
import AppScatterChart from '../components/dataDisplay/AppScatterChart.svelte'
import AppTimeseries from '../components/dataDisplay/AppTimeseries.svelte'
import AppHtml from '../components/dataDisplay/AppHtml.svelte'
import AppSliderInputs from '../components/numberInputs/AppSliderInputs.svelte'
import AppFormButton from '../components/form/AppFormButton.svelte'
import VegaLiteHtml from '../components/dataDisplay/VegaLiteHtml.svelte'
import PlotlyHtml from '../components/dataDisplay/PlotlyHtml.svelte'
export let component: AppComponent
export let selected: boolean
export let locked: boolean = false
export let pointerdown: boolean = false
let hover = false
const { staticOutputs, mode, connectingInput } = getContext<AppEditorContext>('AppEditorContext')
</script>
<div
on:pointerenter={() => (hover = true)}
on:pointerleave={() => (hover = false)}
class="h-full flex flex-col w-full component"
>
{#if $mode !== 'preview'}
<ComponentHeader {hover} {pointerdown} {component} {selected} on:delete on:lock {locked} />
{/if}
<div
on:pointerdown={(e) => {
if ($mode === 'preview') {
e?.stopPropagation()
}
}}
class={classNames(
'border h-full bg-white',
selected && $mode !== 'preview' ? 'border-blue-500' : 'border-white',
!selected && $mode !== 'preview' && !component.card ? 'border-gray-100' : '',
$mode !== 'preview' && !$connectingInput.opened ? 'hover:border-blue-500' : '',
component.softWrap ? '' : 'overflow-auto',
$mode != 'preview' ? 'cursor-pointer' : '',
'relative z-auto'
)}
>
{#if component.type === 'displaycomponent'}
<DisplayComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'barchartcomponent'}
<BarChartComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'timeseriescomponent'}
<AppTimeseries
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'htmlcomponent'}
<AppHtml
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'vegalitecomponent'}
<VegaLiteHtml
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'plotlycomponent'}
<PlotlyHtml
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'scatterchartcomponent'}
<AppScatterChart
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'piechartcomponent'}
<PieChartComponent
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
bind:componentInput={component.componentInput}
/>
{:else if component.type === 'tablecomponent'}
<TableComponent
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
bind:componentInput={component.componentInput}
bind:actionButtons={component.actionButtons}
/>
{:else if component.type === 'textcomponent'}
<TextComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'buttoncomponent'}
<ButtonComponent
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'selectcomponent'}
<SelectComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'formcomponent'}
<AppForm
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'formbuttoncomponent'}
<AppFormButton
{...component}
bind:componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'checkboxcomponent'}
<CheckboxComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'textinputcomponent'}
<TextInputComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'passwordinputcomponent'}
<TextInputComponent
inputType="password"
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'dateinputcomponent'}
<DateInputComponent
inputType="date"
{...component}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'numberinputcomponent'}
<NumberInputComponent {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'slidercomponent'}
<AppSliderInputs {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{/if}
</div>
</div>

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import type { AppEditorContext } from '../types'
import type { AppComponent, AppEditorContext } from '../types'
import { Anchor, Bug, Move } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import Popover from '$lib/components/Popover.svelte'
import { Alert, Button } from '$lib/components/common'
import type { AppComponent } from './Component.svelte'
export let component: AppComponent
export let selected: boolean

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { getContext, afterUpdate } from 'svelte'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../types'
import Grid from 'svelte-grid'
import ComponentEditor from './ComponentEditor.svelte'
import { classNames } from '$lib/utils'
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
import type { Policy } from '$lib/gen'
import HiddenComponent from '../components/HiddenComponent.svelte'
import Component from './Component.svelte'
export let policy: Policy
@@ -100,20 +100,6 @@
const onpointerup = () => {
pointerdown = false
}
afterUpdate(() => {
if ($selectedComponent) {
const parents = document.querySelectorAll<HTMLElement>('.svlt-grid-item')
parents.forEach((parent) => {
const hasActiveChild = !!parent.querySelector('.active-grid-item')
if (hasActiveChild) {
parent.style.setProperty('z-index', '100')
} else {
parent.style.removeProperty('z-index')
}
})
}
})
</script>
<div class="pb-2 relative z-20">
@@ -163,14 +149,15 @@
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:pointerdown={() => selectComponent(dataItem.data.id)}
on:pointerdown={() => {
selectComponent(dataItem.data.id)
}}
class={classNames(
'h-full w-full center-center',
$selectedComponent === dataItem.data.id ? 'active-grid-item' : '',
'h-full w-full flex justify-center align-center items-center',
gridComponent.data.card ? 'border border-gray-100' : ''
)}
>
<Component
<ComponentEditor
{pointerdown}
bind:component={gridComponent.data}
selected={$selectedComponent === dataItem.data.id}
@@ -204,12 +191,12 @@
{/each}
{/if}
<style global>
.svlt-grid-shadow {
<style>
:global(.svlt-grid-shadow) {
/* Back shadow */
background: #93c4fdd0 !important;
background: rgb(147 197 253) !important;
}
.svlt-grid-active {
:global(.svlt-grid-active) {
opacity: 1 !important;
}
</style>

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppEditorContext } from '../types'
import type { TableComponent } from './Component.svelte'
import type { AppEditorContext, TableComponent } from '../types'
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
export let component: TableComponent

View File

@@ -1,25 +1,59 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import Icon from 'svelte-awesome'
import type { AppEditorContext, GridItem } from '../../types'
import type { AppComponent, AppEditorContext, GridItem } from '../../types'
import { displayData } from '../../utils'
import { componentSets } from './data'
import gridHelp from 'svelte-grid/build/helper/index.mjs'
import { getContext, onMount } from 'svelte'
import { getNextId } from '$lib/components/flows/flowStateUtils'
import type { Size } from 'svelte-grid'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { isOpenStore } from './store'
import { gridColumns } from '../../gridUtils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import {
components as componentsRecord,
componentSets,
getRecommendedDimensionsByComponent,
type AppComponent
} from '../Component.svelte'
const { app, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
function addComponent(appComponentType: AppComponent['type']) {
function getRecommendedDimensionsByComponent(
componentType: AppComponent['type'],
column: number
): Size {
// Dimensions key formula: <mobile width>:<mobile height>-<desktop width>:<desktop height>
const dimensions: Record<`${number}:${number}-${number}:${number}`, AppComponent['type'][]> = {
'1:1-3:1': ['textcomponent'],
'1:1-2:1': ['buttoncomponent', 'checkboxcomponent'],
'1:2-1:2': ['htmlcomponent'],
'2:1-3:1': [
'textinputcomponent',
'numberinputcomponent',
'selectcomponent',
'passwordinputcomponent',
'dateinputcomponent'
],
'3:5-6:5': ['formcomponent'],
'2:8-6:8': [
'timeseriescomponent',
'barchartcomponent',
'piechartcomponent',
'displaycomponent',
'scatterchartcomponent',
'vegalitecomponent',
'plotlycomponent'
],
'3:10-6:10': ['tablecomponent']
}
// Finds the key that is associated with the component type and extracts the dimensions from it
const [dimension] = Object.entries(dimensions).find(([_, value]) =>
value.includes(componentType)
) || ['2:1-2:1']
const size = dimension.split('-')[column === 3 ? 0 : 1].split(':')
return { w: +size[0], h: +size[1] }
}
function addComponent(appComponent: AppComponent) {
$dirtyStore = true
const grid = $app.grid ?? []
const id = getNextId(
@@ -27,8 +61,6 @@
true
)
const appComponent = componentsRecord[appComponentType].data
appComponent.id = id
const newComponent = {
@@ -90,13 +122,13 @@
{#each components as item}
<button
on:click={() => addComponent(item)}
title={componentsRecord[item].name}
title={displayData[item.type].name}
class="border w-24 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
justify-center bg-white rounded-md hover:bg-gray-100 duration-200"
>
<svelte:component this={componentsRecord[item].icon} />
<svelte:component this={displayData[item.type].icon} />
<div class="text-xs w-full text-center ellipsize">
{componentsRecord[item].name}
{displayData[item.type].name}
</div>
</button>
{/each}

View File

@@ -0,0 +1,597 @@
import type { ComponentSet } from '../../types'
import { defaultAlignement } from './componentDefaultProps'
const inputs: ComponentSet = {
title: 'Inputs',
components: [
{
softWrap: true,
verticalAlignment: 'center',
id: 'textinputcomponent',
type: 'textinputcomponent',
componentInput: undefined,
configuration: {
placeholder: {
type: 'static',
value: 'Type...',
fieldType: 'text',
onlyStatic: true
},
defaultValue: {
type: 'static',
value: undefined,
fieldType: 'text'
}
},
card: false
},
{
softWrap: true,
verticalAlignment: 'center',
id: 'passwordinputcomponent',
type: 'passwordinputcomponent',
componentInput: undefined,
configuration: {
placeholder: {
type: 'static',
value: 'Password',
fieldType: 'text',
onlyStatic: true
}
},
card: false
},
{
softWrap: true,
verticalAlignment: 'center',
id: 'numberinputcomponent',
type: 'numberinputcomponent',
componentInput: undefined,
configuration: {
placeholder: {
type: 'static',
value: 'Type...',
fieldType: 'text',
onlyStatic: true
},
defaultValue: {
type: 'static',
value: undefined,
fieldType: 'number'
}
},
card: false
},
{
softWrap: true,
verticalAlignment: 'center',
id: 'slidercomponent',
type: 'slidercomponent',
componentInput: undefined,
configuration: {
min: {
type: 'static',
value: 0,
fieldType: 'number',
onlyStatic: true
},
max: {
type: 'static',
value: 42,
fieldType: 'number',
onlyStatic: true
}
},
card: false
},
{
softWrap: true,
verticalAlignment: 'center',
id: 'dateinputcomponent',
type: 'dateinputcomponent',
componentInput: undefined,
configuration: {
minDate: {
type: 'static',
value: '',
fieldType: 'date'
},
maxDate: {
type: 'static',
value: '',
fieldType: 'date'
},
defaultValue: {
type: 'static',
value: undefined,
fieldType: 'date'
}
},
card: false
},
{
...defaultAlignement,
softWrap: true,
id: 'checkboxcomponent',
type: 'checkboxcomponent',
componentInput: undefined,
configuration: {
label: {
type: 'static',
value: 'Label',
fieldType: 'text'
},
defaultValue: {
type: 'static',
value: undefined,
fieldType: 'boolean'
}
},
card: false
},
{
verticalAlignment: 'center',
id: 'selectcomponent',
type: 'selectcomponent',
componentInput: undefined,
configuration: {
items: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [
{ value: 'foo', label: 'Foo' },
{ value: 'bar', label: 'Bar' }
]
},
itemKey: {
type: 'static',
fieldType: 'text',
value: 'value'
}
},
card: false,
softWrap: true
}
]
}
const buttons: ComponentSet = {
title: 'Buttons',
components: [
{
...defaultAlignement,
softWrap: true,
id: 'buttoncomponent',
type: 'buttoncomponent',
componentInput: {
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined
},
recomputeIds: undefined,
configuration: {
label: {
type: 'static',
fieldType: 'text',
value: 'Press me'
},
color: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
optionValuesKey: 'buttonColorOptions',
value: 'blue'
},
size: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
optionValuesKey: 'buttonSizeOptions',
value: 'xs'
},
disabled: {
fieldType: 'boolean',
type: 'eval',
expr: 'false'
}
},
card: false
},
{
horizontalAlignment: 'center',
id: 'formcomponent',
type: 'formcomponent',
componentInput: {
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined
},
recomputeIds: undefined,
configuration: {
label: {
type: 'static',
value: 'Submit',
fieldType: 'text'
},
color: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
value: 'dark',
optionValuesKey: 'buttonColorOptions'
},
size: {
fieldType: 'select',
type: 'static',
value: 'xs',
onlyStatic: true,
optionValuesKey: 'buttonSizeOptions'
}
},
card: true
},
{
horizontalAlignment: 'center',
verticalAlignment: 'center',
id: 'formbuttoncomponent',
type: 'formbuttoncomponent',
componentInput: {
type: 'runnable',
fieldType: 'any',
fields: {},
runnable: undefined
},
recomputeIds: undefined,
configuration: {
label: {
type: 'static',
value: 'Open popup',
fieldType: 'text'
},
color: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
value: 'dark',
optionValuesKey: 'buttonColorOptions'
},
size: {
fieldType: 'select',
type: 'static',
value: 'xs',
onlyStatic: true,
optionValuesKey: 'buttonSizeOptions'
}
},
card: true
}
]
}
const display: ComponentSet = {
title: 'Display',
components: [
{
softWrap: false,
id: 'htmlcomponent',
type: 'htmlcomponent',
componentInput: {
type: 'static',
fieldType: 'template',
value: `<img
src="https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1024&amp;h=1280&amp;q=80"
>
<h1 class="absolute top-4 left-2 text-white">
Hello \${ctx.username}
</h1>`
},
configuration: {},
card: false
},
{
softWrap: false,
id: 'vegalitecomponent',
type: 'vegalitecomponent',
componentInput: {
type: 'static',
fieldType: 'object',
value: {
data: {
values: [
{ a: 'A', b: 28 },
{ a: 'B', b: 55 },
{ a: 'C', b: 43 },
{ a: 'D', b: 91 }
]
},
mark: 'bar',
encoding: {
x: { field: 'a', type: 'ordinal' },
y: { field: 'b', type: 'quantitative' }
}
}
},
configuration: {
canvas: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false,
tooltip: 'use the canvas renderer instead of the svg one for more interactive plots'
}
},
card: false
},
{
softWrap: false,
id: 'plotlycomponent',
type: 'plotlycomponent',
componentInput: {
type: 'static',
fieldType: 'object',
value: {
type: 'bar',
x: [1, 2, 3, 4],
y: [5, 10, 2, 8],
marker: {
color: '#C8A2C8',
line: {
width: 2.5
}
}
}
},
configuration: {},
card: false
},
{
softWrap: true,
horizontalAlignment: 'left',
verticalAlignment: 'top',
id: 'textcomponent',
type: 'textcomponent',
componentInput: {
type: 'static',
fieldType: 'template',
value: 'Hello ${ctx.username}'
},
configuration: {
style: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
optionValuesKey: 'textStyleOptions',
value: 'Body'
},
extraStyle: {
type: 'static',
fieldType: 'text',
value: '',
tooltip: 'CSS rules like "color: blue;"'
},
copyButton: {
type: 'static',
value: false,
fieldType: 'boolean',
onlyStatic: true
}
},
card: false
},
{
id: 'tablecomponent',
type: 'tablecomponent',
configuration: {
search: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
optionValuesKey: 'tableSearchOptions',
value: 'Disabled'
}
},
componentInput: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [
{
id: 1,
name: 'A cell with a long name',
age: 42
},
{
id: 2,
name: 'A briefer cell',
age: 84
}
]
},
card: true,
actionButtons: []
},
{
id: 'piechartcomponent',
type: 'piechartcomponent',
configuration: {
theme: {
type: 'static',
onlyStatic: true,
fieldType: 'select',
optionValuesKey: 'chartThemeOptions',
value: 'theme1'
},
doughnutStyle: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
}
},
componentInput: {
type: 'static',
fieldType: 'object',
value: { data: [25, 50, 25], labels: ['Pie', 'Charts', '<3'] }
},
card: true
},
{
id: 'barchartcomponent',
type: 'barchartcomponent',
configuration: {
theme: {
type: 'static',
onlyStatic: true,
fieldType: 'select',
optionValuesKey: 'chartThemeOptions',
value: 'theme1'
},
line: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
}
},
componentInput: {
type: 'static',
fieldType: 'object',
value: { data: [25, 50, 25], labels: ['Bar', 'Charts', '<3'] }
},
card: true
},
{
id: 'scatterchartcomponent',
type: 'scatterchartcomponent',
configuration: {
zoomable: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
},
pannable: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
}
},
componentInput: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [
{
label: 'foo',
data: [
{ x: 25, y: 50 },
{ x: 23, y: 23 },
{ x: 12, y: 37 }
],
backgroundColor: 'rgb(255, 12, 137)'
},
{
label: 'foobar',
data: [
{ x: 32, y: 32 },
{ x: 25, y: 42 },
{ x: 3, y: 27 }
],
backgroundColor: 'orange'
}
]
},
card: true
},
{
id: 'timeseriescomponent',
type: 'timeseriescomponent',
configuration: {
logarithmicScale: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
},
zoomable: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
},
pannable: {
type: 'static',
onlyStatic: true,
fieldType: 'boolean',
value: false
}
},
componentInput: {
type: 'static',
fieldType: 'array',
subFieldType: 'object',
value: [
{
label: 'foo',
data: [
{
x: '2021-11-06 23:39:30',
y: 50
},
{
x: '2021-11-07 01:00:28',
y: 60
},
{
x: '2021-11-07 09:00:28',
y: 20
}
],
backgroundColor: 'rgb(255, 12, 137)'
},
{
label: 'foobar',
data: [
{
x: '2021-11-06 23:39:30',
y: 20
},
{
x: '2021-11-07 01:00:28',
y: 13
},
{
x: '2021-11-07 09:00:28',
y: 45
}
],
backgroundColor: 'orange'
}
]
},
card: true
},
{
id: 'displaycomponent',
type: 'displaycomponent',
componentInput: {
type: 'static',
fieldType: 'object',
value: { foo: 42 }
},
configuration: {},
card: false
}
]
}
const componentSets = [buttons, inputs, display]
export { componentSets }

View File

@@ -1,8 +1,10 @@
<script lang="ts">
import { page } from '$app/stores'
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import { key } from 'svelte-awesome/icons'
import type { AppEditorContext } from '../../types'
import { components } from '../Component.svelte'
import { displayData } from '../../utils'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
@@ -29,7 +31,7 @@
const component = $app.grid.find((c) => c?.data?.id === componentId)
if (component?.data.type) {
return components[component?.data.type].name
return displayData[component?.data.type].name
} else if (componentId == 'ctx') {
return 'Context'
} else if (componentId.startsWith('bg_')) {

View File

@@ -7,8 +7,8 @@
import { capitalize, emptySchema } from '$lib/utils'
import { createEventDispatcher, getContext } from 'svelte'
import { fly } from 'svelte/transition'
import { defaultCode } from '../../editorUtils'
import type { AppEditorContext } from '../../types'
import { defaultCode } from '../Component.svelte'
export let name: string
export let id: string

View File

@@ -1,6 +1,5 @@
import type { Runnable } from "../../inputType"
import type { GridItem } from "../../types"
import type { AppComponent } from "../Component.svelte";
import type { AppComponent, GridItem } from "../../types"
export interface AppScriptsList {
inline: { name: string; id: string }[],
@@ -31,11 +30,11 @@ export function getAppScripts(grid: GridItem[]) {
}
return acc
}, { inline: [], imported: [] } as AppScriptsList)
}, {inline: [], imported: []} as AppScriptsList)
}
function processRunnable(runnable: Runnable, id: string, list: AppScriptsList) {
if (runnable?.type === undefined) { return }
if(runnable?.type === undefined) { return }
const type: keyof AppScriptsList = runnable.type === 'runnableByPath' ? 'imported' : 'inline'
list[type].push({
name: runnable[runnable.type === 'runnableByPath' ? 'path' : 'name'],

View File

@@ -8,7 +8,7 @@
AlignStartHorizontal,
AlignStartVertical
} from 'lucide-svelte'
import type { AppComponent } from '../Component.svelte'
import type { AppComponent } from '../../types'
import PanelSection from './common/PanelSection.svelte'
export let component: AppComponent

View File

@@ -2,7 +2,7 @@
import Button from '$lib/components/common/button/Button.svelte'
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import type { AppComponent, AppEditorContext } from '../../types'
import PanelSection from './common/PanelSection.svelte'
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
import TableActions from './TableActions.svelte'
@@ -17,7 +17,6 @@
import AlignmentEditor from './AlignmentEditor.svelte'
import RunnableInputEditor from './inputEditor/RunnableInputEditor.svelte'
import TemplateEditor from '$lib/components/TemplateEditor.svelte'
import type { AppComponent } from '../Component.svelte'
export let component: AppComponent | undefined
export let onDelete: (() => void) | undefined = undefined

View File

@@ -6,8 +6,7 @@
import { faPlus, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import type { AppEditorContext, BaseAppComponent } from '../../types'
import type { ButtonComponent } from '../Component.svelte'
import type { ButtonComponent, AppEditorContext, BaseAppComponent } from '../../types'
import PanelSection from './common/PanelSection.svelte'
import TableActionLabel from './TableActionLabel.svelte'

View File

@@ -0,0 +1,346 @@
import type { AppComponent } from './types'
export function defaultCode(component: string, language: string): string | undefined {
return DEFAULT_CODES[component]?.[language]
}
const DEFAULT_CODES: Partial<Record<AppComponent['type'], Record<'deno' | 'python3', string>>> = {
tablecomponent: {
deno:
`export async function main() {
return [
{
"id": 1,
"name": "A cell with a long name",
"age": 42
},
{
"id": 2,
"name": "A briefer cell",
"age": 84
}
]
}`,
python3:
`def main():
return [
{
"id": 1,
"name": "A cell with a long name",
"age": 42
},
{
"id": 2,
"name": "A briefer cell",
"age": 84
}
]`,
},
textcomponent: {
deno:
`export async function main() {
return "foo"
}`,
python3:
`def main():
return "foo"`,
},
barchartcomponent: {
deno:
`export async function main() {
return {
"data": [
25,
50,
25
],
"labels": [
"Bar",
"Charts",
"<3"
]
}
}`,
python3:
`def main():
return {
"data": [
25,
50,
25
],
"labels": [
"Bar",
"Charts",
"<3"
]
}`,
},
displaycomponent: {
deno:
`export async function main() {
return {
"foo": 42
}
}`,
python3:
`def main():
return {
"foo": 42
}`,
},
htmlcomponent: {
deno:
`export async function main() {
return \`<img
src="https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1024&amp;h=1280&amp;q=80"
>
<h1 class="absolute top-4 left-2 text-white">
Hello world
</h1>\`
}`,
python3:
`def main():
return '''<img
src="https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1024&amp;h=1280&amp;q=80"
>
<h1 class="absolute top-4 left-2 text-white">
Hello world
</h1>'''`,
},
vegalitecomponent: {
deno:
`
export async function main() {
return {
data: {
values: [
{ a: "A", b: 28 },
{ a: "B", b: 55 },
{ a: "C", b: 43 },
{ a: "D", b: 91 },
],
},
mark: "bar",
encoding: {
x: { field: "a", type: "ordinal" },
y: { field: "b", type: "quantitative" },
},
};
}
`,
python3:
`
def main():
return {
"data": {
"values": [
{ "a": "A", "b": 28 },
{ "a": "B", "b": 55 },
{ "a": "C", "b": 43 },
{ "a": "D", "b": 91 },
],
},
"mark": "bar",
"encoding": {
"x": { "field": "a", "type": "ordinal" },
"y": { "field": "b", "type": "quantitative" },
},
}
`
},
plotlycomponent: {
deno:
`
export async function main() {
return {
type: 'bar',
x: [1, 2, 3, 4],
y: [5, 10, 2, 8],
marker: {
color: '#C8A2C8',
line: {
width: 2.5
}
}
};
}
`,
python3:
`
def main():
return {
"type": "bar",
"x": [1, 2, 3, 4],
"y": [5, 10, 2, 8],
"marker": {
"color": "#C8A2C8",
"line": {
"width": 2.5
}
}
}
`
},
piechartcomponent: {
deno:
`export async function main() {
return {
"data": [
25,
50,
25
],
"labels": [
"Pie",
"Charts",
"<3"
]
}
}`,
python3:
`def main():
return {
"data": [
25,
50,
25
],
"labels": [
"Pie",
"Charts",
"<3"
]
}`,
},
scatterchartcomponent: {
deno:
`export async function main() {
return [
{
"label": "foo",
"data": [
{ "x": 25, "y": 50 },
{ "x": 23, "y": 23 },
{ "x": 12, "y": 37 }
],
"backgroundColor": "rgb(255, 12, 137)"
},
{
"label": "bar",
"data": [
{ "x": 32, "y": 32 },
{ "x": 25, "y": 42 },
{ "x": 3, "y": 27 }
],
"backgroundColor": "orange"
}
]
}`,
python3:
`def main():
return [
{
"label": "foo",
"data": [
{ "x": 25, "y": 50 },
{ "x": 23, "y": 23 },
{ "x": 12, "y": 37 }
],
"backgroundColor": "rgb(255, 12, 137)"
},
{
"label": "bar",
"data": [
{ "x": 32, "y": 32 },
{ "x": 25, "y": 42 },
{ "x": 3, "y": 27 }
],
"backgroundColor": "orange"
}
]`,
},
timeseriescomponent: {
deno:
`export async function main() {
return [
{
"label": "foo",
"data": [
{
"x": "2021-11-06 23:39:30",
"y": 50
},
{
"x": "2021-11-07 01:00:28",
"y": 60
},
{
"x": "2021-11-07 09:00:28",
"y": 20
}
],
"backgroundColor": "rgb(255, 12, 137)"
},
{
"label": "bar",
"data": [
{
"x": "2021-11-06 23:39:30",
"y": 20
},
{
"x": "2021-11-07 01:00:28",
"y": 13
},
{
"x": "2021-11-07 09:00:28",
"y": 45
}
],
"backgroundColor": "orange"
}
]
}`,
python3:
`def main():
return [
{
"label": "foo",
"data": [
{
"x": "2021-11-06 23:39:30",
"y": 50
},
{
"x": "2021-11-07 01:00:28",
"y": 60
},
{
"x": "2021-11-07 09:00:28",
"y": 20
}
],
"backgroundColor": "rgb(255, 12, 137)"
},
{
"label": "bar",
"data": [
{
"x": "2021-11-06 23:39:30",
"y": 20
},
{
"x": "2021-11-07 01:00:28",
"y": 13
},
{
"x": "2021-11-07 09:00:28",
"y": 45
}
],
"backgroundColor": "orange"
}
]`,
},
} as const

View File

@@ -2,7 +2,6 @@ import type { Schema } from '$lib/common'
import type { Preview } from '$lib/gen'
import type { FilledItem } from 'svelte-grid'
import type { Writable } from 'svelte/store'
import type { AppComponent } from './editor/Component.svelte'
import type {
AppInput,
ConnectedAppInput,
@@ -14,6 +13,46 @@ import type {
} from './inputType'
import type { World } from './rx'
type BaseComponent<T extends string> = {
type: T
}
export type TextComponent = BaseComponent<'textcomponent'>
export type TextInputComponent = BaseComponent<'textinputcomponent'>
export type PasswordInputComponent = BaseComponent<'passwordinputcomponent'>
export type DateInputComponent = BaseComponent<'dateinputcomponent'>
export type NumberInputComponent = BaseComponent<'numberinputcomponent'>
export type SliderComponent = BaseComponent<'slidercomponent'>
export type HtmlComponent = BaseComponent<'htmlcomponent'>
export type VegaLiteComponent = BaseComponent<'vegalitecomponent'>
export type PlotlyComponent = BaseComponent<'plotlycomponent'>
export type TimeseriesComponent = BaseComponent<'timeseriescomponent'>
export type ButtonComponent = BaseComponent<'buttoncomponent'> & {
recomputeIds: string[] | undefined
}
export type FormComponent = BaseComponent<'formcomponent'> & {
recomputeIds: string[] | undefined
}
export type FormButtonComponent = BaseComponent<'formbuttoncomponent'> & {
recomputeIds: string[] | undefined
}
export type RunFormComponent = BaseComponent<'runformcomponent'>
export type BarChartComponent = BaseComponent<'barchartcomponent'>
export type PieChartComponent = BaseComponent<'piechartcomponent'>
export type ScatterChartComponent = BaseComponent<'scatterchartcomponent'>
export type TableComponent = BaseComponent<'tablecomponent'> & {
actionButtons: (BaseAppComponent & ButtonComponent)[]
}
export type DisplayComponent = BaseComponent<'displaycomponent'>
export type ImageComponent = BaseComponent<'imagecomponent'>
export type InputComponent = BaseComponent<'inputcomponent'>
export type SelectComponent = BaseComponent<'selectcomponent'>
export type CheckboxComponent = BaseComponent<'checkboxcomponent'>
export type RadioComponent = BaseComponent<'radiocomponent'>
export type HorizontalAlignment = 'left' | 'center' | 'right'
export type VerticalAlignment = 'top' | 'center' | 'bottom'
@@ -43,11 +82,38 @@ export interface BaseAppComponent extends Partial<Aligned> {
softWrap?: boolean
}
export type AppComponent = BaseAppComponent &
(
| RunFormComponent
| DisplayComponent
| TextInputComponent
| PasswordInputComponent
| DateInputComponent
| NumberInputComponent
| SliderComponent
| BarChartComponent
| TimeseriesComponent
| HtmlComponent
| TableComponent
| TextComponent
| TableComponent
| ButtonComponent
| PieChartComponent
| ScatterChartComponent
| ImageComponent
| InputComponent
| SelectComponent
| CheckboxComponent
| RadioComponent
| FormComponent
| FormButtonComponent
| VegaLiteComponent
| PlotlyComponent
)
export type ComponentSet = {
title: string
components: AppComponent['type'][]
components: AppComponent[]
}
type SectionID = string

View File

@@ -2,11 +2,31 @@ import type { Schema } from '$lib/common'
import { FlowService, ScriptService } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { emptySchema } from '$lib/utils'
import type { AppComponent } from './editor/Component.svelte'
import {
BarChart4,
Binary,
CircleDot,
FormInput,
Inspect,
List,
Monitor,
PieChart,
Play,
Table2,
Image,
TextCursorInput,
Type,
Lock,
Calendar,
ToggleLeft,
GripHorizontal,
Code2,
SlidersHorizontal,
PlusSquare
} from 'lucide-svelte'
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
import type { Output } from './rx'
import type { App, GridItem } from './types'
import type { App, AppComponent, GridItem } from './types'
export async function loadSchema(
workspace: string,
@@ -65,7 +85,104 @@ export function schemaToInputsSpec(
}, {})
}
export const displayData: Record<AppComponent['type'], { name: string; icon: any }> = {
displaycomponent: {
name: 'Result',
icon: Monitor
},
textcomponent: {
name: 'Text',
icon: Type
},
buttoncomponent: {
name: 'Button',
icon: Inspect
},
formcomponent: {
name: 'Form',
icon: FormInput
},
formbuttoncomponent: {
name: 'Modal Form',
icon: PlusSquare
},
piechartcomponent: {
name: 'Pie Chart',
icon: PieChart
},
barchartcomponent: {
name: 'Bar/Line Chart',
icon: BarChart4
},
htmlcomponent: {
name: 'Html',
icon: Code2
},
vegalitecomponent: {
name: 'Vega Lite',
icon: PieChart
},
plotlycomponent: {
name: 'Plotly',
icon: PieChart
},
timeseriescomponent: {
name: 'Timeseries',
icon: GripHorizontal
},
scatterchartcomponent: {
name: 'Scatter Chart',
icon: GripHorizontal
},
tablecomponent: {
name: 'Table',
icon: Table2
},
checkboxcomponent: {
name: 'Toggle',
icon: ToggleLeft
},
textinputcomponent: {
name: 'Text input',
icon: TextCursorInput
},
imagecomponent: {
name: 'Image',
icon: Image
},
inputcomponent: {
name: 'Input',
icon: FormInput
},
radiocomponent: {
name: 'Radio button',
icon: CircleDot
},
runformcomponent: {
name: 'Run form',
icon: Play
},
selectcomponent: {
name: 'Select',
icon: List
},
numberinputcomponent: {
name: 'Number',
icon: Binary
},
slidercomponent: {
name: 'Slider',
icon: SlidersHorizontal
},
passwordinputcomponent: {
name: 'Password',
icon: Lock
},
dateinputcomponent: {
name: 'Date input',
icon: Calendar
}
}
export function accessPropertyByPath<T>(object: T, path: string): T | undefined {
// convert indexes to properties

View File

@@ -6,7 +6,7 @@ import { sendUserToast } from './utils.js'
export async function logoutWithRedirect(rd?: string): Promise<void> {
await clearUser()
if (rd && rd != "/" && rd?.split('?')[0] != '/user/login') {
if (rd && rd?.split('?')[0] != '/user/login') {
const error = document.cookie.includes('token')
? `error=${encodeURIComponent('You have been logged out because your session has expired.')}&`
: ''

View File

@@ -23,7 +23,7 @@
})
oauthStore.set({ access_token: token })
sendUserToast(
'Slack workspace connected to your Windmill workspace and slack token saved in the folder `slack_bot` at `f/slack_bot/bot_token`.'
'Slack workspace connected to your Windmill workspace and slack token saved at `g/slack/bot_token`.'
)
} else {
sendUserToast('Missing code or state as query params', true)

View File

@@ -6,7 +6,7 @@
import { slide } from 'svelte/transition'
import { OauthService, UserService, WorkspaceService } from '$lib/gen'
import { clearStores, usersWorkspaceStore, workspaceStore, userStore } from '$lib/stores'
import { isCloudHosted, sendUserToast } from '$lib/utils'
import { sendUserToast } from '$lib/utils'
import CenteredModal from '$lib/components/CenteredModal.svelte'
import { getUserExt, refreshSuperadmin } from '$lib/user'
import { Button, Skeleton } from '$lib/components/common'
@@ -169,12 +169,11 @@
</div>
{#if showPassword}
<div transition:slide>
{#if isCloudHosted()}
<p class="text-xs text-gray-500 italic pb-6">
To get credentials without the OAuth providers above, send an email at
contact@windmill.dev
</p>
{/if}
<p class="text-xs text-gray-500 italic pb-6">
To get credentials without the OAuth providers above, you can send us an email at
contact@windmill.dev or your admin owners if this instance is self-hosted and you will
receive credentials that you can use below.
</p>
<label class="block pb-2">
<span class="text-gray-700 text-sm">Email</span>
<input type="email" bind:value={email} id="email" />
@@ -188,15 +187,4 @@
</div>
</div>
{/if}
{#if isCloudHosted()}
<p class="text-2xs text-gray-500 italic mt-10 text-center">
By logging in, you agree to our <a
href="https://docs.windmill.dev/terms_of_service"
target="_blank">Terms of Service</a
>
and
<a href="https://docs.windmill.dev/privacy_policy" target="_blank">Privacy Policy</a>
</p>
{/if}
</CenteredModal>

View File

@@ -47,14 +47,9 @@
let nbDisplayed = 30
let plan: string | undefined = undefined
let customer_id: string | undefined = undefined
let webhook: string | undefined = undefined
let tab =
($page.url.searchParams.get('tab') as
| 'users'
| 'slack'
| 'premium'
| 'export_delete'
| 'webhook') ?? 'users'
($page.url.searchParams.get('tab') as 'users' | 'slack' | 'premium' | 'export_delete') ??
'users'
// function getDropDownItems(username: string): DropdownItem[] {
// return [
@@ -89,23 +84,6 @@
sendUserToast(`slack command script set to ${scriptPath}`)
}
async function editWebhook(): Promise<void> {
// in JS, an empty string is also falsy
if (webhook) {
await WorkspaceService.editWebhook({
workspace: $workspaceStore!,
requestBody: { webhook }
})
sendUserToast(`webhook set to ${webhook}`)
} else {
await WorkspaceService.editWebhook({
workspace: $workspaceStore!,
requestBody: { webhook: undefined }
})
sendUserToast(`webhook removed`)
}
}
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
team_name = settings.slack_name
@@ -115,7 +93,6 @@
plan = settings.plan
customer_id = settings.customer_id
initialPath = scriptPath
webhook = settings.webhook
}
async function listUsers(): Promise<void> {
@@ -212,7 +189,7 @@
<Tab size="md" value="users">
<div class="flex gap-2 items-center my-1"> Users & Invites </div>
</Tab>
<Tab size="md" value="slack">
<Tab size="md" value="slack">
<div class="flex gap-2 items-center my-1"> Slack Command </div>
</Tab>
{#if isCloudHosted()}
@@ -220,14 +197,11 @@
<div class="flex gap-2 items-center my-1"> Premium Plans </div>
</Tab>
{/if}
<Tab size="md" value="export_delete">
<div class="flex gap-2 items-center my-1"> Export & Delete Workspace </div>
</Tab>
<Tab size="md" value="webhook">
<div class="flex gap-2 items-center my-1">Webhook for CLI Sync</div>
</Tab>
</Tabs>
</div>
<Tab size="md" value="export_delete">
<div class="flex gap-2 items-center my-1"> Export & Delete Workspace </div>
</Tab>
</Tabs>
</div>
{#if tab == 'users'}
<PageHeader title="Members ({users?.length ?? ''})" primary={false} />
@@ -711,29 +685,6 @@
</Button>
{/if}
</div>
{:else if tab == 'webhook'}
<PageHeader title="Webhook for CLI Sync" primary={false} />
<div class="mt-2"
><Alert type="info" title="Send events to an external service"
>Connect your windmill workspace to an external service to sync or get notified about any
changes</Alert
></div
>
<h3 class="mt-5 text-gray-700"
>URL to send requests to<Tooltip>
This URL will be POSTed to with a JSON body depending on the type of event. The type is
indicated by the <pre>type</pre> field. The other fields are dependent on the type.
</Tooltip>
</h3>
<div class="flex gap-2">
<input class="justify-start" type="text" bind:value={webhook} />
<Button color="blue" btnClasses="justify-end" size="md" on:click={editWebhook}
>Set Webhook</Button
>
</div>
{/if}
{:else}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4" role="alert">

View File

@@ -46,7 +46,7 @@ declare module 'svelte-grid' {
default: { item: ItemLayout; dataItem: Item<T> & { data: AppComponent } }
}
export default class Grid<T = {}> extends SvelteComponentTyped<Props<T>, {}, Slots<T>> { }
export default class Grid<T = {}> extends SvelteComponentTyped<Props<T>, {}, Slots<T>> {}
}
declare module 'svelte-grid/build/helper/index.mjs' {

View File

@@ -244,6 +244,7 @@ const config = {
padding: `${theme('spacing.1')} ${theme('spacing.2')}`,
border: `1px solid ${theme('colors.gray.300')}`,
borderRadius: theme('borderRadius.md'),
boxShadow: theme('boxShadow.sm'),
'&:focus': {
'--tw-ring-color': theme('colors.indigo.100'),
'--tw-ring-offset-shadow':

View File

@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.62.0"
wmill_pg = ">=1.62.0"
wmill = ">=1.61.1"
wmill_pg = ">=1.61.1"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.62.0
version: 1.61.1
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.62.0"
version = "1.61.1"
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.62.0"
windmill-api = "^1.61.1"
[build-system]
requires = ["poetry>=1.0.2", "poetry-dynamic-versioning"]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.62.0"
version = "1.61.1"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1 +1 @@
1.62.0
1.61.1