Compare commits
55 Commits
v1.61.1
...
rf/exportI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46ddbcc254 | ||
|
|
fcab562bf2 | ||
|
|
56616d5c6d | ||
|
|
471ec6b334 | ||
|
|
56dd32b2f0 | ||
|
|
3f8916cbc2 | ||
|
|
ac991dddbc | ||
|
|
083a304645 | ||
|
|
91491055fa | ||
|
|
ae440203f0 | ||
|
|
ab432d628a | ||
|
|
e1b9247e11 | ||
|
|
07c756f460 | ||
|
|
2ef6af4546 | ||
|
|
a939771059 | ||
|
|
8dc467b87a | ||
|
|
2ece1eb475 | ||
|
|
7a4da3907f | ||
|
|
b9d6e67791 | ||
|
|
f584062f13 | ||
|
|
265fbc5835 | ||
|
|
2e7e57b62d | ||
|
|
d17eeeecdc | ||
|
|
21c2007ebd | ||
|
|
90668902f5 | ||
|
|
784aac9d1b | ||
|
|
d4207db880 | ||
|
|
4ac9484305 | ||
|
|
0a8f177e02 | ||
|
|
cfa1e6f1e8 | ||
|
|
be526b2f23 | ||
|
|
8bc97e0041 | ||
|
|
b9ac60f8bb | ||
|
|
c0a8545704 | ||
|
|
cdd16195ae | ||
|
|
406cba4e73 | ||
|
|
8d6a8386be | ||
|
|
1a626980df | ||
|
|
23007f7a71 | ||
|
|
9f5500c196 | ||
|
|
a82a2efa6a | ||
|
|
3305481d5d | ||
|
|
42691bc1bd | ||
|
|
99568eaa47 | ||
|
|
68500b12b2 | ||
|
|
f171cd8b7c | ||
|
|
0ca431b6cb | ||
|
|
cb9c0846ac | ||
|
|
bc8d1a375e | ||
|
|
0921ba0085 | ||
|
|
14b32be8b2 | ||
|
|
b67550a790 | ||
|
|
06347746c0 | ||
|
|
0b4288ad9c | ||
|
|
a6dcb218ff |
2
.github/workflows/docker-310.yml
vendored
2
.github/workflows/docker-310.yml
vendored
@@ -6,7 +6,7 @@ name: Build and push windmill with python 3.10 and openbb
|
||||
on: workflow_dispatch
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
group: ${{ github.ref }}-openbb
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -1,6 +1,26 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
14
Caddyfile
14
Caddyfile
@@ -1,5 +1,15 @@
|
||||
{$BASE_URL} {
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
http://{$BASE_URL} {
|
||||
bind {$ADDRESS}
|
||||
reverse_proxy /ws/* http://lsp:3001
|
||||
reverse_proxy /* http://windmill_server:8000
|
||||
reverse_proxy /* http://windmill:8000
|
||||
}
|
||||
|
||||
https://{$BASE_URL} {
|
||||
bind {$ADDRESS}
|
||||
reverse_proxy /ws/* http://localhost:3001
|
||||
}
|
||||
}
|
||||
|
||||
13
README.md
13
README.md
@@ -182,7 +182,7 @@ compiling from source or using without a postgres super user, see
|
||||
`docker compose up` with the following docker-compose is sufficient:
|
||||
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
|
||||
|
||||
Go to https://localhost et voilà :)
|
||||
Go to http://localhost et voilà :)
|
||||
|
||||
For older kernels < 4.18, set `DISABLE_NUSER=true` as env variable, otherwise
|
||||
nsjail will not be able to launch the isolated scripts.
|
||||
@@ -278,7 +278,8 @@ 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) | Worker |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -308,9 +309,11 @@ upcoming CLI tool.
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
|
||||
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
|
||||
| |
|
||||
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | Worker |
|
||||
| |
|
||||
| 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 |
|
||||
|
||||
|
||||
|
||||
|
||||
186
backend/Cargo.lock
generated
186
backend/Cargo.lock
generated
@@ -408,9 +408,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.3.0"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
|
||||
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
|
||||
|
||||
[[package]]
|
||||
name = "cargo-lock"
|
||||
@@ -661,9 +661,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.88"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "322296e2f2e5af4270b54df9e85a02ff037e271af20ba3e7fe1575515dc840b8"
|
||||
checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
@@ -673,9 +673,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.88"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "017a1385b05d631e7875b1f151c9f012d37b53491e2a87f65bff5c262b2111d8"
|
||||
checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
@@ -688,15 +688,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.88"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c26bbb078acf09bc1ecda02d4223f03bdd28bd4874edcb0379138efc499ce971"
|
||||
checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.88"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357f40d1f06a24b60ae1fe122542c1fb05d28d32acb2aed064e84bc2ad1e252e"
|
||||
checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -928,9 +928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.31"
|
||||
version = "0.8.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
|
||||
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
@@ -992,7 +992,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"windows-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1308,9 +1308,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -1429,9 +1429,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.23"
|
||||
version = "0.14.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
|
||||
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -1538,12 +1538,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
|
||||
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1574,7 +1574,7 @@ dependencies = [
|
||||
"hermit-abi 0.2.6",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1603,9 +1603,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.60"
|
||||
version = "0.3.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
|
||||
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -1907,7 +1907,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[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.6",
|
||||
"parking_lot_core 0.9.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2187,15 +2187,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.6"
|
||||
version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf"
|
||||
checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2462,7 +2462,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "progenitor"
|
||||
version = "0.2.1-dev"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"built",
|
||||
@@ -2480,7 +2480,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "progenitor-client"
|
||||
version = "0.2.1-dev"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
|
||||
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#8a09b98645276ef3738d3e904e1c32e5de1d657e"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"heck",
|
||||
@@ -2516,7 +2516,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "progenitor-macro"
|
||||
version = "0.2.1-dev"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#8a09b98645276ef3738d3e904e1c32e5de1d657e"
|
||||
source = "git+https://github.com/oxidecomputer/progenitor#88ff3cf1e056a8c3901f17e73ae0d1fba991a17f"
|
||||
dependencies = [
|
||||
"openapiv3",
|
||||
"proc-macro2",
|
||||
@@ -2863,7 +2863,7 @@ dependencies = [
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2890,7 +2890,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#b6e447145800ec7888f035bf266329e4b457e5f9"
|
||||
source = "git+https://github.com/RustPython/RustPython#adc23253e4b58980b407ba2760dbe61681d752fc"
|
||||
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#b6e447145800ec7888f035bf266329e4b457e5f9"
|
||||
source = "git+https://github.com/RustPython/RustPython#adc23253e4b58980b407ba2760dbe61681d752fc"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bitflags",
|
||||
@@ -2916,7 +2916,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#b6e447145800ec7888f035bf266329e4b457e5f9"
|
||||
source = "git+https://github.com/RustPython/RustPython#adc23253e4b58980b407ba2760dbe61681d752fc"
|
||||
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",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2979,7 +2979,7 @@ dependencies = [
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid 1.2.2",
|
||||
"uuid 1.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3458,7 +3458,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
"uuid 1.2.2",
|
||||
"uuid 1.3.0",
|
||||
"webpki-roots",
|
||||
"whoami",
|
||||
]
|
||||
@@ -3707,9 +3707,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
@@ -3843,9 +3843,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
@@ -3865,7 +3865,7 @@ dependencies = [
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4172,7 +4172,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
[[package]]
|
||||
name = "typify"
|
||||
version = "0.0.11-dev"
|
||||
source = "git+https://github.com/oxidecomputer/typify#cd1f95b66b0d0fbb33537d66043f26512ee3268b"
|
||||
source = "git+https://github.com/oxidecomputer/typify#77a43da6b1e9a79fe3ec719a038f4ea949c0342e"
|
||||
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#cd1f95b66b0d0fbb33537d66043f26512ee3268b"
|
||||
source = "git+https://github.com/oxidecomputer/typify#77a43da6b1e9a79fe3ec719a038f4ea949c0342e"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"log",
|
||||
@@ -4199,7 +4199,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typify-macro"
|
||||
version = "0.0.11-dev"
|
||||
source = "git+https://github.com/oxidecomputer/typify#cd1f95b66b0d0fbb33537d66043f26512ee3268b"
|
||||
source = "git+https://github.com/oxidecomputer/typify#77a43da6b1e9a79fe3ec719a038f4ea949c0342e"
|
||||
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.2.2",
|
||||
"uuid 1.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4392,9 +4392,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
|
||||
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
|
||||
dependencies = [
|
||||
"getrandom 0.2.8",
|
||||
"serde",
|
||||
@@ -4478,9 +4478,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.83"
|
||||
version = "0.2.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
|
||||
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
@@ -4488,9 +4488,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.83"
|
||||
version = "0.2.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
|
||||
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -4503,9 +4503,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.33"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
|
||||
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -4515,9 +4515,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.83"
|
||||
version = "0.2.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
|
||||
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -4525,9 +4525,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.83"
|
||||
version = "0.2.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
|
||||
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4538,9 +4538,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.83"
|
||||
version = "0.2.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
|
||||
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
@@ -4557,9 +4557,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.60"
|
||||
version = "0.3.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
|
||||
checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -4638,7 +4638,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4665,7 +4665,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4682,8 +4682,10 @@ dependencies = [
|
||||
"hmac",
|
||||
"hyper",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"mime_guess",
|
||||
"prometheus",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"retainer",
|
||||
@@ -4717,7 +4719,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"chrono",
|
||||
@@ -4727,12 +4729,12 @@ dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"uuid 1.2.2",
|
||||
"uuid 1.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -4745,7 +4747,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4764,12 +4766,12 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid 1.2.2",
|
||||
"uuid 1.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4777,7 +4779,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4791,7 +4793,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4803,7 +4805,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4818,7 +4820,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deno_core",
|
||||
@@ -4832,7 +4834,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4848,14 +4850,14 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tracing",
|
||||
"ulid",
|
||||
"uuid 1.2.2",
|
||||
"uuid 1.3.0",
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -4876,7 +4878,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid 1.2.2",
|
||||
"uuid 1.3.0",
|
||||
"windmill-api-client",
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
@@ -4903,6 +4905,30 @@ 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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.61.1"
|
||||
version = "1.62.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
1
backend/migrations/20230126023323_webhook.down.sql
Normal file
1
backend/migrations/20230126023323_webhook.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
5
backend/migrations/20230126023323_webhook.up.sql
Normal file
5
backend/migrations/20230126023323_webhook.up.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE
|
||||
workspace_settings
|
||||
ADD
|
||||
COLUMN webhook text;
|
||||
1
backend/migrations/20230204182500_add_mem_peak.down.sql
Normal file
1
backend/migrations/20230204182500_add_mem_peak.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
3
backend/migrations/20230204182500_add_mem_peak.up.sql
Normal file
3
backend/migrations/20230204182500_add_mem_peak.up.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE queue ADD COLUMN mem_peak INTEGER;
|
||||
ALTER TABLE completed_job ADD COLUMN mem_peak INTEGER;
|
||||
@@ -117,17 +117,25 @@
|
||||
},
|
||||
"query": "SELECT script_path FROM queue WHERE id = $1"
|
||||
},
|
||||
"062859f1d0e5cfba3115f4241115753b86a4ad239708851c998ff5620ebca5b8": {
|
||||
"0715955b7e98cc669a88eca6556cd46b7f9c07fcf32a24f85b69720b54e6e95f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "mem_peak",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE queue SET last_ping = now() WHERE id = $1"
|
||||
"query": "SELECT mem_peak FROM queue WHERE id = $1"
|
||||
},
|
||||
"07551a32c49da8c0693dd39c6a63b5b2a596ccc0e52e8918160604a5e133dd32": {
|
||||
"describe": {
|
||||
@@ -409,28 +417,6 @@
|
||||
},
|
||||
"query": "INSERT INTO password(email, verified, password_hash, login_type, super_admin, name, company)\n VALUES ($1, $2, $3, 'password', $4, $5, $6)"
|
||||
},
|
||||
"11eb4dd4a2c9b0b759294dde5e8b505c5a4391aa0d8cb629c665711ee0fc04a0": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "logs",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3"
|
||||
},
|
||||
"122090a0f89e5248a0a0f199ebd24582fdb302883aebd2da187ac0084e767ea3": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -558,6 +544,11 @@
|
||||
"name": "plan",
|
||||
"ordinal": 8,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "webhook",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -569,6 +560,7 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@@ -1231,6 +1223,18 @@
|
||||
},
|
||||
"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": [
|
||||
@@ -1477,6 +1481,74 @@
|
||||
},
|
||||
"query": "UPDATE app SET versions = array_append(versions, $1) WHERE id = $2"
|
||||
},
|
||||
"41659beec0e8d6d95258a728e8e4b2b929045a05bd0cfb2293031c0e4a62f04e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
"Numeric",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Int4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n , mem_peak\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)"
|
||||
},
|
||||
"42e1b5634a9e51247115fa73f85a97b1467c913d012cac9c45bb6a349082dc71": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1850,6 +1922,11 @@
|
||||
"name": "plan",
|
||||
"ordinal": 8,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "webhook",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
@@ -1861,6 +1938,7 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
@@ -2710,6 +2788,19 @@
|
||||
},
|
||||
"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": [],
|
||||
@@ -3359,6 +3450,27 @@
|
||||
},
|
||||
"query": "UPDATE schedule SET path = $1, script_path = $1 WHERE path = $2 AND workspace_id = $3 AND is_flow IS true RETURNING *"
|
||||
},
|
||||
"913c634de21d46b8841f8a7c25c408da7c572f9e685db6351848cbf6e9253efc": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "canceled",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled"
|
||||
},
|
||||
"924e1d5cbb682db0d6121dd911f38cf8948d0e59ca47428daa4f43fce686ba80": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3765,6 +3877,26 @@
|
||||
},
|
||||
"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": [
|
||||
@@ -3800,73 +3932,6 @@
|
||||
},
|
||||
"query": "UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"a46d97b0c65bbbcc29022c2d010a4853541987a58f0098f9c2e3fd6e40b11a52": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
"Numeric",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO completed_job AS cj\n ( workspace_id\n , id\n , parent_job\n , created_by\n , created_at\n , started_at\n , duration_ms\n , success\n , script_hash\n , script_path\n , args\n , result\n , logs\n , raw_code\n , raw_lock\n , canceled\n , canceled_by\n , canceled_reason\n , job_kind\n , schedule_path\n , permissioned_as\n , flow_status\n , raw_flow\n , is_flow_step\n , is_skipped\n , language\n , email\n , visible_to_owner\n )\n VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28)\n ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)"
|
||||
},
|
||||
"a6145b0482c9e5da245059a80b1563cad20318fd2dd8aef33f9ca97de1826b8b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -4314,6 +4379,34 @@
|
||||
},
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at FROM token WHERE email = $1\n ORDER BY created_at DESC"
|
||||
},
|
||||
"b69f747eae8b13a0a8d6914a3a7ad322554fcfe62cd28c5f0a475f18dd770d61": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "logs",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "mem_peak",
|
||||
"ordinal": 1,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3"
|
||||
},
|
||||
"b7dd791cd69748ef51b7520f505c0c8bb1b4014a273476eddfecf1ab658a18b4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -70,3 +70,5 @@ cookie.workspace = true
|
||||
sha2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
async-stripe.workspace = true
|
||||
lazy_static.workspace = true
|
||||
prometheus.workspace = true
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.61.1
|
||||
version: 1.62.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -883,6 +883,8 @@ paths:
|
||||
type: string
|
||||
customer_id:
|
||||
type: string
|
||||
webhook:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/workspaces/premium_info:
|
||||
get:
|
||||
@@ -962,6 +964,33 @@ 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
|
||||
@@ -3263,6 +3292,8 @@ paths:
|
||||
type: boolean
|
||||
new_logs:
|
||||
type: string
|
||||
mem_peak:
|
||||
type: integer
|
||||
|
||||
/w/{workspace}/jobs/completed/get/{id}:
|
||||
get:
|
||||
@@ -4761,6 +4792,8 @@ components:
|
||||
type: string
|
||||
visible_to_owner:
|
||||
type: boolean
|
||||
mem_peak:
|
||||
type: integer
|
||||
required:
|
||||
- id
|
||||
- running
|
||||
@@ -4847,6 +4880,8 @@ components:
|
||||
type: string
|
||||
visible_to_owner:
|
||||
type: boolean
|
||||
mem_peak:
|
||||
type: integer
|
||||
required:
|
||||
- id
|
||||
- created_by
|
||||
|
||||
@@ -12,6 +12,7 @@ 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},
|
||||
@@ -310,6 +311,7 @@ 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)> {
|
||||
@@ -356,7 +358,12 @@ 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))
|
||||
}
|
||||
@@ -395,6 +402,7 @@ 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();
|
||||
@@ -418,6 +426,10 @@ 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))
|
||||
}
|
||||
@@ -425,6 +437,7 @@ 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>,
|
||||
@@ -514,6 +527,14 @@ 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))
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
schedule::clear_schedule,
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -181,6 +182,7 @@ 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)> {
|
||||
@@ -221,6 +223,10 @@ 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(
|
||||
@@ -280,6 +286,7 @@ 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> {
|
||||
@@ -368,6 +375,14 @@ 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(
|
||||
@@ -450,6 +465,7 @@ 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();
|
||||
@@ -474,6 +490,10 @@ 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"))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -139,6 +140,7 @@ 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> {
|
||||
@@ -193,8 +195,12 @@ async fn create_folder(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::CreateFolder { workspace: w_id, name: ng.name.clone() },
|
||||
);
|
||||
|
||||
Ok(format!("Created folder {}", ng.name))
|
||||
}
|
||||
|
||||
@@ -245,6 +251,7 @@ 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> {
|
||||
@@ -298,8 +305,12 @@ 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() },
|
||||
);
|
||||
|
||||
Ok(format!("Updated folder {}", name))
|
||||
}
|
||||
|
||||
@@ -416,6 +427,7 @@ 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?;
|
||||
@@ -440,6 +452,12 @@ 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))
|
||||
}
|
||||
|
||||
@@ -447,6 +465,7 @@ 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> {
|
||||
@@ -477,6 +496,12 @@ 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))
|
||||
}
|
||||
|
||||
@@ -510,6 +535,7 @@ 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> {
|
||||
@@ -540,5 +566,11 @@ 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))
|
||||
}
|
||||
|
||||
@@ -258,6 +258,8 @@ pub struct CompletedJob {
|
||||
pub is_skipped: bool,
|
||||
pub email: String,
|
||||
pub visible_to_owner: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mem_peak: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -270,6 +272,14 @@ pub struct RunJobQuery {
|
||||
queue_limit: Option<i64>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref INCLUDE_HEADERS: Vec<String> = std::env::var("INCLUDE_HEADERS")
|
||||
.ok().map(|x| x
|
||||
.split(',')
|
||||
.map(|s| s.to_string())
|
||||
.collect()).unwrap_or_default();
|
||||
}
|
||||
|
||||
impl RunJobQuery {
|
||||
async fn get_scheduled_for<'c>(
|
||||
&self,
|
||||
@@ -290,11 +300,14 @@ impl RunJobQuery {
|
||||
headers: HeaderMap,
|
||||
mut args: serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
self.include_header
|
||||
let whitelist = self
|
||||
.include_header
|
||||
.as_ref()
|
||||
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
whitelist
|
||||
.iter()
|
||||
.chain(INCLUDE_HEADERS.iter())
|
||||
.for_each(|h| {
|
||||
if let Some(v) = headers.get(h) {
|
||||
args.insert(
|
||||
@@ -461,7 +474,7 @@ async fn list_jobs(
|
||||
"running",
|
||||
"script_hash",
|
||||
"script_path",
|
||||
"args",
|
||||
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
|
||||
"null as duration_ms",
|
||||
"null as success",
|
||||
"false as deleted",
|
||||
@@ -476,6 +489,7 @@ async fn list_jobs(
|
||||
"email",
|
||||
"visible_to_owner",
|
||||
"suspend",
|
||||
"mem_peak",
|
||||
],
|
||||
);
|
||||
let sqlc = list_completed_jobs_query(
|
||||
@@ -495,7 +509,7 @@ async fn list_jobs(
|
||||
"null as running",
|
||||
"script_hash",
|
||||
"script_path",
|
||||
"args",
|
||||
"CASE WHEN pg_column_size(args) > 1000 THEN '\"too large args\"'::jsonb ELSE args END",
|
||||
"duration_ms",
|
||||
"success",
|
||||
"deleted",
|
||||
@@ -510,6 +524,7 @@ async fn list_jobs(
|
||||
"email",
|
||||
"visible_to_owner",
|
||||
"null as suspend",
|
||||
"mem_peak",
|
||||
],
|
||||
);
|
||||
let sql = format!(
|
||||
@@ -987,6 +1002,7 @@ struct UnifiedJob {
|
||||
email: String,
|
||||
visible_to_owner: bool,
|
||||
suspend: Option<i32>,
|
||||
mem_peak: Option<i32>,
|
||||
}
|
||||
|
||||
impl From<UnifiedJob> for Job {
|
||||
@@ -1021,6 +1037,7 @@ impl From<UnifiedJob> for Job {
|
||||
is_skipped: uj.is_skipped,
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
mem_peak: uj.mem_peak,
|
||||
}),
|
||||
"QueuedJob" => Job::QueuedJob(QueuedJob {
|
||||
workspace_id: uj.workspace_id,
|
||||
@@ -1053,6 +1070,7 @@ impl From<UnifiedJob> for Job {
|
||||
email: uj.email,
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
suspend: uj.suspend,
|
||||
mem_peak: uj.mem_peak,
|
||||
}),
|
||||
t => panic!("job type {} not valid", t),
|
||||
}
|
||||
@@ -1571,6 +1589,7 @@ pub struct JobUpdate {
|
||||
pub running: Option<bool>,
|
||||
pub completed: Option<bool>,
|
||||
pub new_logs: Option<String>,
|
||||
pub mem_peak: Option<i32>,
|
||||
}
|
||||
|
||||
async fn get_job_update(
|
||||
@@ -1580,8 +1599,8 @@ async fn get_job_update(
|
||||
) -> error::JsonResult<JobUpdate> {
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let logs = query_scalar!(
|
||||
"SELECT substr(logs, $1) as logs FROM queue WHERE workspace_id = $2 AND id = $3",
|
||||
let record = sqlx::query!(
|
||||
"SELECT substr(logs, $1) as logs, mem_peak FROM queue WHERE workspace_id = $2 AND id = $3",
|
||||
log_offset,
|
||||
&w_id,
|
||||
&id
|
||||
@@ -1589,12 +1608,13 @@ async fn get_job_update(
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(logs) = logs {
|
||||
if let Some(record) = record {
|
||||
tx.commit().await?;
|
||||
Ok(Json(JobUpdate {
|
||||
running: if !running { Some(true) } else { None },
|
||||
completed: None,
|
||||
new_logs: logs,
|
||||
new_logs: record.logs,
|
||||
mem_peak: record.mem_peak,
|
||||
}))
|
||||
} else {
|
||||
let logs = query_scalar!(
|
||||
@@ -1612,6 +1632,7 @@ async fn get_job_update(
|
||||
running: Some(false),
|
||||
completed: Some(true),
|
||||
new_logs: logs,
|
||||
mem_peak: record.map(|r| r.mem_peak).flatten(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1729,6 +1750,7 @@ async fn list_completed_jobs(
|
||||
"is_skipped",
|
||||
"email",
|
||||
"visible_to_owner",
|
||||
"mem_peak",
|
||||
],
|
||||
)
|
||||
.sql()?;
|
||||
|
||||
@@ -21,6 +21,7 @@ use crate::{
|
||||
oauth2::{build_oauth_clients, SlackVerifier},
|
||||
tracing_init::{MyMakeSpan, MyOnResponse},
|
||||
users::{Authed, OptAuthed},
|
||||
webhook_util::WebhookShared,
|
||||
};
|
||||
|
||||
mod apps;
|
||||
@@ -42,6 +43,7 @@ mod tracing_init;
|
||||
mod users;
|
||||
mod utils;
|
||||
mod variables;
|
||||
mod webhook_util;
|
||||
mod worker_ping;
|
||||
mod workspaces;
|
||||
|
||||
@@ -106,7 +108,8 @@ pub async fn run_server(
|
||||
std::env::var("COOKIE_DOMAIN").ok(),
|
||||
))))
|
||||
.layer(Extension(http_client))
|
||||
.layer(CookieManagerLayer::new());
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.nest(
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -263,6 +264,7 @@ 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)> {
|
||||
@@ -293,6 +295,11 @@ 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),
|
||||
@@ -302,6 +309,7 @@ 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();
|
||||
@@ -333,12 +341,18 @@ 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>,
|
||||
@@ -400,6 +414,15 @@ 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))
|
||||
}
|
||||
|
||||
@@ -411,6 +434,7 @@ 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> {
|
||||
@@ -436,6 +460,14 @@ 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))
|
||||
}
|
||||
@@ -513,6 +545,7 @@ 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)> {
|
||||
@@ -543,6 +576,11 @@ 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),
|
||||
@@ -574,6 +612,7 @@ 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)?;
|
||||
@@ -598,6 +637,10 @@ 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))
|
||||
}
|
||||
@@ -605,6 +648,7 @@ 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> {
|
||||
@@ -634,6 +678,10 @@ 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))
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
schedule::clear_schedule,
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
@@ -184,6 +185,7 @@ 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>,
|
||||
@@ -400,6 +402,14 @@ 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,
|
||||
@@ -417,6 +427,14 @@ 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?;
|
||||
@@ -600,6 +618,7 @@ 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)>,
|
||||
@@ -626,6 +645,10 @@ 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(())
|
||||
}
|
||||
@@ -633,6 +656,7 @@ 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?;
|
||||
@@ -657,12 +681,18 @@ 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> {
|
||||
@@ -691,6 +721,11 @@ 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))
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
oauth2::{AllClients, _refresh_token},
|
||||
users::{require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
BaseUrl,
|
||||
};
|
||||
/*
|
||||
@@ -224,6 +225,7 @@ 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)> {
|
||||
@@ -265,6 +267,11 @@ 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),
|
||||
@@ -274,6 +281,7 @@ 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();
|
||||
@@ -306,6 +314,11 @@ 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))
|
||||
}
|
||||
|
||||
@@ -320,6 +333,7 @@ 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>,
|
||||
@@ -405,6 +419,15 @@ 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))
|
||||
}
|
||||
|
||||
|
||||
112
backend/windmill-api/src/webhook_util.rs
Normal file
112
backend/windmill-api/src/webhook_util.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ 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))
|
||||
@@ -90,6 +91,7 @@ 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)]
|
||||
@@ -117,6 +119,11 @@ struct EditAutoInvite {
|
||||
operator: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EditWebhook {
|
||||
webhook: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateWorkspace {
|
||||
id: String,
|
||||
@@ -519,6 +526,48 @@ 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>,
|
||||
|
||||
@@ -45,7 +45,7 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
const MAX_FREE_EXECS: i32 = 1000;
|
||||
const MAX_FREE_CONCURRENT_RUNS: i32 = 3;
|
||||
const MAX_FREE_CONCURRENT_RUNS: i32 = 15;
|
||||
|
||||
pub async fn cancel_job<'c>(
|
||||
username: &str,
|
||||
@@ -82,7 +82,32 @@ pub async fn cancel_job<'c>(
|
||||
Ok((tx, job_option))
|
||||
}
|
||||
|
||||
pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<QueuedJob>> {
|
||||
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(",")
|
||||
));
|
||||
}
|
||||
/* Jobs can be started if they:
|
||||
* - haven't been started before,
|
||||
* running = false
|
||||
@@ -90,7 +115,7 @@ pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<
|
||||
* 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>(
|
||||
let job: Option<QueuedJob> = sqlx::query_as::<_, QueuedJob>(&format!(
|
||||
"UPDATE queue
|
||||
SET running = true
|
||||
, started_at = coalesce(started_at, now())
|
||||
@@ -99,17 +124,17 @@ pub async fn pull(db: &Pool<Postgres>) -> windmill_common::error::Result<Option<
|
||||
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()))
|
||||
OR suspend_until <= now()))) {workspaces_filter}
|
||||
ORDER BY scheduled_for
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING *",
|
||||
)
|
||||
RETURNING *"
|
||||
))
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
@@ -646,6 +671,8 @@ pub struct QueuedJob {
|
||||
pub visible_to_owner: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suspend: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mem_peak: Option<i32>,
|
||||
}
|
||||
|
||||
impl QueuedJob {
|
||||
|
||||
@@ -77,6 +77,12 @@ pub async fn add_completed_job(
|
||||
None
|
||||
};
|
||||
|
||||
let mem_peak = sqlx::query_scalar!("SELECT mem_peak FROM queue WHERE id = $1", &queued_job.id)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.flatten();
|
||||
let mut tx = db.begin().await?;
|
||||
let job_id = queued_job.id.clone();
|
||||
sqlx::query!(
|
||||
@@ -109,9 +115,10 @@ pub async fn add_completed_job(
|
||||
, language
|
||||
, email
|
||||
, visible_to_owner
|
||||
, mem_peak
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($26, (EXTRACT('epoch' FROM (now())) - EXTRACT('epoch' FROM (COALESCE($6, now()))))*1000), $7, $8, $9,\
|
||||
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28)
|
||||
$10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $27, $28, $29)
|
||||
ON CONFLICT (id) DO UPDATE SET success = $7, result = $11, logs = concat(cj.logs, $12)",
|
||||
queued_job.workspace_id,
|
||||
queued_job.id,
|
||||
@@ -140,7 +147,8 @@ pub async fn add_completed_job(
|
||||
queued_job.language: ScriptLang,
|
||||
duration: Option<i64>,
|
||||
queued_job.email,
|
||||
queued_job.visible_to_owner
|
||||
queued_job.visible_to_owner,
|
||||
mem_peak
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await
|
||||
|
||||
@@ -26,7 +26,7 @@ use windmill_common::{
|
||||
utils::rd_string,
|
||||
variables,
|
||||
};
|
||||
use windmill_queue::{canceled_job_to_result, get_queued_job, pull, JobKind, QueuedJob};
|
||||
use windmill_queue::{canceled_job_to_result, get_queued_job, pull, JobKind, QueuedJob, CLOUD_HOSTED};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -35,8 +35,7 @@ use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
process::{Child, Command},
|
||||
sync::{
|
||||
mpsc::{self, Sender},
|
||||
oneshot, watch,
|
||||
mpsc::{self, Sender}, watch,
|
||||
},
|
||||
time::{interval, sleep, Instant, MissedTickBehavior},
|
||||
};
|
||||
@@ -317,6 +316,11 @@ lazy_static::lazy_static! {
|
||||
);
|
||||
}
|
||||
|
||||
//only matter if CLOUD_HOSTED
|
||||
const MAX_RESULT_SIZE: usize = 1024 * 1024 * 2; // 2MB
|
||||
|
||||
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub async fn run_worker(
|
||||
db: &Pool<Postgres>,
|
||||
@@ -404,6 +408,13 @@ 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());
|
||||
@@ -420,16 +431,22 @@ pub async fn run_worker(
|
||||
.ok()
|
||||
.map(|x| format!(";{x}"))
|
||||
.unwrap_or_else(|| String::new());
|
||||
let max_log_size = std::env::var("MAX_LOG_SIZE")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<i64>().ok())
|
||||
.unwrap_or(500000);
|
||||
|
||||
|
||||
|
||||
|
||||
let deno_flags = std::env::var("DENO_FLAGS")
|
||||
.ok()
|
||||
.map(|x| x.split(' ').map(|x| x.to_string()).collect());
|
||||
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
|
||||
@@ -443,6 +460,10 @@ pub async fn run_worker(
|
||||
.and_then(|x| x.parse::<i32>().ok())
|
||||
.unwrap_or(100);
|
||||
|
||||
let additional_python_paths = std::env::var("ADDITIONAL_PYTHON_PATHS")
|
||||
.ok()
|
||||
.map(|x| x.split(':').map(|x| x.to_string()).collect());
|
||||
|
||||
let envs = Envs {
|
||||
deno_path,
|
||||
go_path,
|
||||
@@ -453,10 +474,10 @@ pub async fn run_worker(
|
||||
pip_index_url,
|
||||
pip_extra_index_url,
|
||||
pip_trusted_host,
|
||||
max_log_size,
|
||||
deno_flags,
|
||||
deno_auth_tokens,
|
||||
pip_local_dependencies,
|
||||
additional_python_paths,
|
||||
};
|
||||
WORKER_STARTED.inc();
|
||||
|
||||
@@ -476,11 +497,14 @@ 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!(
|
||||
@@ -521,7 +545,9 @@ 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).map(|x| (x, timer)) } => {
|
||||
(job, timer) = {
|
||||
let timer = worker_pull_duration.start_timer();
|
||||
pull(&db, whitelist_workspaces.clone(), blacklist_workspaces.clone()).map(|x| (x, timer)) } => {
|
||||
drop(timer);
|
||||
(false, job)
|
||||
},
|
||||
@@ -531,6 +557,7 @@ pub async fn run_worker(
|
||||
return true;
|
||||
}
|
||||
|
||||
worker_busy.set(1);
|
||||
match next_job {
|
||||
Ok(Some(job)) => {
|
||||
let label_values = [
|
||||
@@ -766,8 +793,8 @@ struct Envs {
|
||||
pip_trusted_host: Option<String>,
|
||||
deno_auth_tokens: String,
|
||||
deno_flags: Option<Vec<String>>,
|
||||
max_log_size: i64,
|
||||
pip_local_dependencies: Option<Vec<String>>,
|
||||
additional_python_paths: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn extract_error_value(log_lines: &str) -> serde_json::Value {
|
||||
@@ -1161,7 +1188,7 @@ mount {{
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_go_job(
|
||||
WorkerConfig { base_internal_url, disable_nuser, disable_nsjail, base_url, .. }: &WorkerConfig,
|
||||
Envs { nsjail_path, go_path, path_env, home_env, max_log_size, .. }: &Envs,
|
||||
Envs { nsjail_path, go_path, path_env, home_env, .. }: &Envs,
|
||||
logs: &mut String,
|
||||
job: &QueuedJob,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
@@ -1199,7 +1226,6 @@ async fn handle_go_job(
|
||||
job_dir,
|
||||
db,
|
||||
timeout,
|
||||
*max_log_size,
|
||||
go_path,
|
||||
true,
|
||||
skip_go_mod,
|
||||
@@ -1320,7 +1346,7 @@ func Run(req Req) (interface{{}}, error){{
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(&job.id, db, logs, timeout, *max_log_size, build_go).await?;
|
||||
handle_child(&job.id, db, logs, timeout, build_go, false).await?;
|
||||
|
||||
Command::new(nsjail_path)
|
||||
.current_dir(job_dir)
|
||||
@@ -1346,14 +1372,14 @@ func Run(req Req) (interface{{}}, error){{
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
|
||||
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_bash_job(
|
||||
WorkerConfig { base_internal_url, disable_nuser, disable_nsjail, base_url, .. }: &WorkerConfig,
|
||||
Envs { nsjail_path, path_env, home_env, max_log_size, .. }: &Envs,
|
||||
Envs { nsjail_path, path_env, home_env, .. }: &Envs,
|
||||
logs: &mut String,
|
||||
job: &QueuedJob,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
@@ -1425,7 +1451,7 @@ async fn handle_bash_job(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
|
||||
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
|
||||
//for now bash jobs have an empty result object
|
||||
Ok(serde_json::json!(logs
|
||||
.lines()
|
||||
@@ -1445,7 +1471,7 @@ fn capitalize(s: &str) -> String {
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_deno_job(
|
||||
WorkerConfig { base_internal_url, base_url, disable_nuser, disable_nsjail, .. }: &WorkerConfig,
|
||||
Envs { nsjail_path, deno_path, path_env, max_log_size, deno_auth_tokens, deno_flags, .. }: &Envs,
|
||||
Envs { nsjail_path, deno_path, path_env, deno_auth_tokens, deno_flags, .. }: &Envs,
|
||||
logs: &mut String,
|
||||
job: &QueuedJob,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
@@ -1589,7 +1615,7 @@ run().catch(async (e) => {{
|
||||
}
|
||||
.instrument(trace_span!("create_deno_jail"))
|
||||
.await?;
|
||||
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
|
||||
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1617,7 +1643,9 @@ lazy_static! {
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_python_job(
|
||||
WorkerConfig { base_internal_url, base_url, disable_nuser, disable_nsjail, .. }: &WorkerConfig,
|
||||
envs @ Envs { nsjail_path, python_path, path_env, max_log_size, .. }: &Envs,
|
||||
envs @ Envs {
|
||||
nsjail_path, python_path, path_env, additional_python_paths, ..
|
||||
}: &Envs,
|
||||
requirements_o: Option<String>,
|
||||
job_dir: &str,
|
||||
worker_dir: &str,
|
||||
@@ -1633,7 +1661,8 @@ async fn handle_python_job(
|
||||
) -> error::Result<serde_json::Value> {
|
||||
create_dependencies_dir(job_dir).await;
|
||||
|
||||
let mut additional_python_paths: Vec<String> = vec![];
|
||||
let mut additional_python_paths: Vec<String> =
|
||||
additional_python_paths.to_owned().unwrap_or_else(|| vec![]);
|
||||
|
||||
let requirements = match requirements_o {
|
||||
Some(r) => r,
|
||||
@@ -1762,12 +1791,13 @@ 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', '')
|
||||
@@ -1859,7 +1889,7 @@ mount {{
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
handle_child(&job.id, db, logs, timeout, *max_log_size, child).await?;
|
||||
handle_child(&job.id, db, logs, timeout, child, !disable_nsjail).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1875,6 +1905,10 @@ async fn read_result(job_dir: &str) -> error::Result<serde_json::Value> {
|
||||
let mut file = File::open(format!("{job_dir}/result.json")).await?;
|
||||
let mut content = "".to_string();
|
||||
file.read_to_string(&mut content).await?;
|
||||
if *CLOUD_HOSTED && content.len() > MAX_RESULT_SIZE {
|
||||
return Err(Error::ExecutionErr("Result is too large for the cloud app (limit 2MB).
|
||||
If using this script as part of the flow, use the shared folder to pass heavy data between steps.".to_owned()));
|
||||
}
|
||||
serde_json::from_str(&content)
|
||||
.map_err(|e| Error::ExecutionErr(format!("Error parsing result: {e}")))
|
||||
}
|
||||
@@ -2055,7 +2089,7 @@ async fn generate_deno_lock(
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
timeout: i32,
|
||||
Envs { deno_path, max_log_size, .. }: &Envs,
|
||||
Envs { deno_path, .. }: &Envs,
|
||||
) -> error::Result<String> {
|
||||
let _ = write_file(job_dir, "main.ts", code).await?;
|
||||
|
||||
@@ -2073,7 +2107,7 @@ async fn generate_deno_lock(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
handle_child(job_id, db, logs, timeout, *max_log_size, child).await?;
|
||||
handle_child(job_id, db, logs, timeout, * child).await?;
|
||||
|
||||
let path_lock = format!("{job_dir}/lock.json");
|
||||
let mut file = File::open(path_lock).await?;
|
||||
@@ -2104,7 +2138,6 @@ async fn capture_dependency_job(
|
||||
job_dir,
|
||||
db,
|
||||
timeout,
|
||||
envs.max_log_size,
|
||||
&envs.go_path,
|
||||
false,
|
||||
false,
|
||||
@@ -2127,7 +2160,7 @@ async fn pip_compile(
|
||||
pip_extra_index_url,
|
||||
pip_index_url,
|
||||
pip_trusted_host,
|
||||
max_log_size,
|
||||
|
||||
pip_local_dependencies,
|
||||
..
|
||||
}: &Envs,
|
||||
@@ -2165,7 +2198,7 @@ async fn pip_compile(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(job_id, db, logs, timeout, *max_log_size, child)
|
||||
handle_child(job_id, db, logs, timeout, child, false)
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
|
||||
let path_lock = format!("{job_dir}/requirements.txt");
|
||||
@@ -2187,7 +2220,6 @@ async fn install_go_dependencies(
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
timeout: i32,
|
||||
max_log_size: i64,
|
||||
go_path: &str,
|
||||
preview: bool,
|
||||
skip_go_mod: bool,
|
||||
@@ -2201,7 +2233,7 @@ async fn install_go_dependencies(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
handle_child(job_id, db, logs, timeout, max_log_size, child).await?;
|
||||
handle_child(job_id, db, logs, timeout, child, false).await?;
|
||||
}
|
||||
let child = Command::new(go_path)
|
||||
.current_dir(job_dir)
|
||||
@@ -2209,7 +2241,7 @@ async fn install_go_dependencies(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(job_id, db, logs, timeout, max_log_size, child)
|
||||
handle_child(job_id, db, logs, timeout, child, false)
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
|
||||
|
||||
@@ -2279,6 +2311,30 @@ async fn get_reserved_variables(
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
|
||||
if pid.is_none() {
|
||||
return -1
|
||||
}
|
||||
let pid = if nsjail {
|
||||
// This is a bit hacky, but the process id of the nsjail process is the pid of nsjail + 1.
|
||||
// Ideally, we would get the number from fork() itself. This works in MOST cases.
|
||||
pid.unwrap() + 1
|
||||
} else {
|
||||
pid.unwrap()
|
||||
};
|
||||
|
||||
if let Ok(file) = File::open(format!("/proc/{}/status", pid)).await {
|
||||
let mut lines = BufReader::new(file).lines();
|
||||
while let Some(line) = lines.next_line().await.unwrap_or(None) {
|
||||
if line.starts_with("VmHWM:") {
|
||||
return line.split_whitespace().nth(1).and_then(|s| s.parse::<i32>().ok()).unwrap_or(-1);
|
||||
};
|
||||
}
|
||||
-2
|
||||
} else {
|
||||
-3
|
||||
}
|
||||
}
|
||||
/// - wait until child exits and return with exit status
|
||||
/// - read lines from stdout and stderr and append them to the "queue"."logs"
|
||||
/// quitting early if output exceedes MAX_LOG_SIZE characters (not bytes)
|
||||
@@ -2291,14 +2347,21 @@ async fn handle_child(
|
||||
db: &Pool<Postgres>,
|
||||
logs: &mut String,
|
||||
timeout: i32,
|
||||
max_log_size: i64,
|
||||
mut child: Child,
|
||||
nsjail: bool,
|
||||
) -> error::Result<()> {
|
||||
let timeout = Duration::from_secs(u64::try_from(timeout).expect("invalid timeout"));
|
||||
let ping_interval = Duration::from_secs(5);
|
||||
let cancel_check_interval = Duration::from_millis(500);
|
||||
let update_job_interval = Duration::from_millis(500);
|
||||
let write_logs_delay = Duration::from_millis(500);
|
||||
|
||||
let pid = child.id();
|
||||
if let Some(pid) = pid {
|
||||
//set the highest oom priority
|
||||
let mut file = File::create(format!("/proc/{pid}/oom_score_adj")).await?;
|
||||
let _ = file.write_all(b"1000").await;
|
||||
} else {
|
||||
tracing::info!("could not get child pid");
|
||||
}
|
||||
let (set_too_many_logs, mut too_many_logs) = watch::channel::<bool>(false);
|
||||
|
||||
let output = child_joined_output_stream(&mut child);
|
||||
@@ -2308,17 +2371,20 @@ async fn handle_child(
|
||||
|
||||
/* the cancellation future is polled on by `wait_on_child` while
|
||||
* waiting for the child to exit normally */
|
||||
let cancel_check = async {
|
||||
let update_job = async {
|
||||
let db = db.clone();
|
||||
|
||||
let mut interval = interval(cancel_check_interval);
|
||||
let mut interval = interval(update_job_interval);
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
tokio::select!(
|
||||
_ = rx.recv() => break,
|
||||
_ = interval.tick() => {
|
||||
if sqlx::query_scalar!("SELECT canceled FROM queue WHERE id = $1", job_id)
|
||||
let mem_peak = get_mem_peak(pid, nsjail).await;
|
||||
tracing::info!("{job_id} still running. mem peak: {}kB", mem_peak);
|
||||
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
|
||||
if sqlx::query_scalar!("UPDATE queue SET mem_peak = GREATEST($1, mem_peak), last_ping = now() WHERE id = $2 RETURNING canceled", mem_peak, job_id)
|
||||
.fetch_optional(&db)
|
||||
.await
|
||||
.map(|v| Some(true) == v)
|
||||
@@ -2348,7 +2414,7 @@ async fn handle_child(
|
||||
biased;
|
||||
result = child.wait() => return result.map(Ok),
|
||||
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
|
||||
_ = cancel_check => KillReason::Cancelled,
|
||||
_ = update_job => KillReason::Cancelled,
|
||||
_ = sleep(timeout) => KillReason::Timeout,
|
||||
};
|
||||
tx.send(()).await.expect("rx should never be dropped");
|
||||
@@ -2382,8 +2448,13 @@ async fn handle_child(
|
||||
|
||||
/* a future that reads output from the child and appends to the database */
|
||||
let lines = async move {
|
||||
let max_log_size = if *CLOUD_HOSTED {
|
||||
MAX_RESULT_SIZE
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
/* log_remaining is zero when output limit was reached */
|
||||
let mut log_remaining = (max_log_size as usize).saturating_sub(logs.chars().count());
|
||||
let mut log_remaining = max_log_size.saturating_sub(logs.chars().count());
|
||||
let mut result = io::Result::Ok(());
|
||||
let mut output = output;
|
||||
/* `do_write` resolves the task, but does not contain the Result.
|
||||
@@ -2414,7 +2485,7 @@ async fn handle_child(
|
||||
tracing::info!(%job_id, "Too many logs lines for job {job_id}");
|
||||
let _ = set_too_many_logs.send(true);
|
||||
joined.push_str(&format!(
|
||||
"Job logs or result reached character limit of {max_log_size}; killing job."
|
||||
"Job logs or result reached character limit of {MAX_RESULT_SIZE}; killing job."
|
||||
));
|
||||
/* stop reading and drop our streams fairly quickly */
|
||||
break;
|
||||
@@ -2471,35 +2542,12 @@ async fn handle_child(
|
||||
}
|
||||
}.instrument(trace_span!("child_lines"));
|
||||
|
||||
/* a stream updating "queue"."last_ping" at an interval */
|
||||
|
||||
let (kill_tx, mut kill_rx) = oneshot::channel::<()>();
|
||||
|
||||
let mut interval = interval(ping_interval);
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let db1 = db.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
if let Err(err) = sqlx::query!("UPDATE queue SET last_ping = now() WHERE id = $1", job_id)
|
||||
.execute(&db1)
|
||||
.await
|
||||
{
|
||||
tracing::error!(%job_id, %err, "error setting last ping for job {job_id}: {err}");
|
||||
};
|
||||
},
|
||||
_ = (&mut kill_rx) => return,
|
||||
}
|
||||
}
|
||||
});
|
||||
let (wait_result, _) = tokio::join!(wait_on_child, lines);
|
||||
kill_tx.send(()).expect("send should always work");
|
||||
|
||||
match wait_result {
|
||||
_ if *too_many_logs.borrow() => Err(Error::ExecutionErr(format!(
|
||||
"logs or result reached limit. Set MAX_LOG_SIZE higher (current: {max_log_size})"
|
||||
"logs or result reached limit. (current max size: {MAX_RESULT_SIZE} characters)"
|
||||
))),
|
||||
Ok(Ok(status)) => {
|
||||
if status.success() {
|
||||
@@ -2712,7 +2760,7 @@ async fn handle_python_reqs(
|
||||
pip_extra_index_url,
|
||||
pip_trusted_host,
|
||||
nsjail_path,
|
||||
max_log_size,
|
||||
|
||||
..
|
||||
}: &Envs,
|
||||
job: &QueuedJob,
|
||||
@@ -2807,7 +2855,7 @@ async fn handle_python_reqs(
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
let child = handle_child(&job.id, db, logs, timeout, *max_log_size, child).await;
|
||||
let child = handle_child(&job.id, db, logs, timeout, child, false).await;
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { colors, setClient } from "./deps.ts";
|
||||
import { tryGetLoginInfo } from "./login.ts";
|
||||
import { colors, setClient, UserService } from "./deps.ts";
|
||||
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import {
|
||||
addWorkspace,
|
||||
getActiveWorkspace,
|
||||
getWorkspaceByName,
|
||||
removeWorkspace,
|
||||
Workspace,
|
||||
} from "./workspace.ts";
|
||||
|
||||
@@ -67,6 +69,27 @@ 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(
|
||||
|
||||
8
cli/decoverto.ts
Normal file
8
cli/decoverto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// 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 };
|
||||
38
cli/deps.ts
38
cli/deps.ts
@@ -1,32 +1,40 @@
|
||||
// windmill
|
||||
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";
|
||||
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";
|
||||
|
||||
// cliffy
|
||||
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 { 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 {
|
||||
DenoLandProvider,
|
||||
UpgradeCommand,
|
||||
} from "https://deno.land/x/cliffy@v0.25.6/command/upgrade/mod.ts";
|
||||
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
|
||||
|
||||
// std
|
||||
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 { 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 {
|
||||
copy,
|
||||
readAll,
|
||||
readerFromStreamReader,
|
||||
} from "https://deno.land/std@0.170.0/streams/mod.ts";
|
||||
export { DelimiterStream } from "https://deno.land/std@0.170.0/streams/mod.ts";
|
||||
} 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";
|
||||
|
||||
// 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";
|
||||
|
||||
169
cli/flow.ts
169
cli/flow.ts
@@ -1,16 +1,145 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import {
|
||||
Difference,
|
||||
GlobalOptions,
|
||||
PushDiffs,
|
||||
Resource,
|
||||
setValueByPath,
|
||||
} from "./types.ts";
|
||||
import {
|
||||
colors,
|
||||
Command,
|
||||
Flow,
|
||||
FlowModule,
|
||||
FlowService,
|
||||
JobService,
|
||||
OpenFlow,
|
||||
microdiff,
|
||||
OpenFlowWPath,
|
||||
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;
|
||||
|
||||
@@ -30,38 +159,10 @@ export async function pushFlow(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
) {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
const data = decoverto.type(FlowFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
await data.push(workspace, remotePath);
|
||||
}
|
||||
|
||||
async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
||||
|
||||
174
cli/folder.ts
174
cli/folder.ts
@@ -1,6 +1,122 @@
|
||||
import { colors, Command, Folder, FolderService } from "./deps.ts";
|
||||
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import { GlobalOptions } from "./types.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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -21,61 +137,15 @@ 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,
|
||||
) {
|
||||
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 data = decoverto.type(FolderFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
data.push(workspace, remotePath);
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
|
||||
14
cli/hub.ts
14
cli/hub.ts
@@ -1,6 +1,6 @@
|
||||
import { Command } from "./deps.ts";
|
||||
import { requireLogin, resolveWorkspace } from "./context.ts";
|
||||
import { pushResourceTypeDef } from "./resource-type.ts";
|
||||
import { ResourceTypeFile } from "./resource-type.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
|
||||
async function pull(opts: GlobalOptions) {
|
||||
@@ -56,14 +56,10 @@ async function pull(opts: GlobalOptions) {
|
||||
const x of list
|
||||
) {
|
||||
console.log("syncing " + x.name);
|
||||
await pushResourceTypeDef(
|
||||
workspace.workspaceId,
|
||||
x.name,
|
||||
{
|
||||
description: x.description,
|
||||
schema: JSON.parse(x.schema),
|
||||
},
|
||||
);
|
||||
const f = new ResourceTypeFile();
|
||||
f.description = x.description;
|
||||
f.schema = JSON.parse(x.schema);
|
||||
await f.push(workspace.workspaceId, x.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
cli/main.ts
18
cli/main.ts
@@ -8,13 +8,14 @@ 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 folder from "./folder.ts";
|
||||
import sync from "./sync.ts";
|
||||
import { tryResolveVersion } from "./context.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
|
||||
const VERSION = "v1.61.1";
|
||||
const VERSION = "v1.62.0";
|
||||
|
||||
const command = new Command()
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
.description("A simple CLI tool for windmill.")
|
||||
.globalOption(
|
||||
@@ -32,10 +33,9 @@ const command = new Command()
|
||||
.command("resource", resource)
|
||||
.command("user", user)
|
||||
.command("variable", variable)
|
||||
.command("push", push)
|
||||
.command("pull", pull)
|
||||
.command("hub", hub)
|
||||
// .command("folder", folder)
|
||||
.command("folder", folder)
|
||||
.command("sync", sync)
|
||||
.command("version", "Show version information")
|
||||
.action(async (opts) => {
|
||||
console.log("CLI build against " + VERSION);
|
||||
@@ -61,6 +61,12 @@ const command = 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) {
|
||||
|
||||
71
cli/pull.ts
71
cli/pull.ts
@@ -1,20 +1,11 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { resolveWorkspace } from "./context.ts";
|
||||
import { GlobalOptions } from "./types.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);
|
||||
import { colors, Command, readerFromStreamReader, Untar } from "./deps.ts";
|
||||
import { Workspace } from "./workspace.ts";
|
||||
|
||||
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");
|
||||
@@ -35,51 +26,29 @@ async function pull(opts: GlobalOptions & { override: boolean }, dir: string) {
|
||||
),
|
||||
);
|
||||
console.log(await tarResponse.text());
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const streamReader = tarResponse.body?.getReader();
|
||||
if (!streamReader) {
|
||||
console.log(colors.red("Failed to read tar request body"));
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
console.log(colors.yellow("Streaming tarball to disk..."));
|
||||
const denoReader = readerFromStreamReader(streamReader);
|
||||
const untar = new Untar(denoReader);
|
||||
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."));
|
||||
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.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
@@ -87,6 +56,6 @@ const command = new Command()
|
||||
"Pull all definitions in the current workspace from the API and write them to disk.",
|
||||
)
|
||||
.arguments("<dir:string>")
|
||||
.action(pull as any);
|
||||
.action(stub as any);
|
||||
|
||||
export default command;
|
||||
|
||||
261
cli/push.ts
261
cli/push.ts
@@ -1,266 +1,21 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
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 { colors, Command } from "./deps.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { pushVariable } from "./variable.ts";
|
||||
import { pushResourceType } from "./resource-type.ts";
|
||||
import { pushFolder } from "./folder.ts";
|
||||
|
||||
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);
|
||||
async function stub(
|
||||
_opts: GlobalOptions,
|
||||
_dir?: string,
|
||||
) {
|
||||
console.log(
|
||||
colors.blue(
|
||||
"Found " + (normal.length + resourceTypes.length + folders.length) +
|
||||
" candidates",
|
||||
colors.red.underline(
|
||||
'Push is deprecated. Use "sync push --raw" instead. See <TODO_LINK_HERE> for more information.',
|
||||
),
|
||||
);
|
||||
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(push as any);
|
||||
.action(stub as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -1,65 +1,129 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import {
|
||||
Difference,
|
||||
GlobalOptions,
|
||||
PushDiffs,
|
||||
Resource as ResourceI,
|
||||
setValueByPath,
|
||||
} from "./types.ts";
|
||||
import { requireLogin, resolveWorkspace } from "./context.ts";
|
||||
import { colors, Command, ResourceService, Table } from "./deps.ts";
|
||||
import {
|
||||
colors,
|
||||
Command,
|
||||
EditResourceType,
|
||||
microdiff,
|
||||
ResourceService,
|
||||
ResourceType,
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||
|
||||
type ResourceTypeFile = {
|
||||
@model()
|
||||
export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
@property(Any)
|
||||
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 = 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
const data: ResourceTypeFile = decoverto.type(ResourceTypeFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
await data.push(workspace, name);
|
||||
}
|
||||
|
||||
type PushOptions = GlobalOptions;
|
||||
|
||||
194
cli/resource.ts
194
cli/resource.ts
@@ -1,80 +1,142 @@
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import {
|
||||
Difference,
|
||||
GlobalOptions,
|
||||
PushDiffs,
|
||||
Resource as Resource2,
|
||||
setValueByPath,
|
||||
} from "./types.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import { colors, Command, Resource, ResourceService, Table } from "./deps.ts";
|
||||
import {
|
||||
colors,
|
||||
Command,
|
||||
EditResource,
|
||||
microdiff,
|
||||
Resource,
|
||||
ResourceService,
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||
|
||||
type ResourceFile = {
|
||||
value: any;
|
||||
@model()
|
||||
export class ResourceFile implements Resource2, PushDiffs {
|
||||
@property(Any)
|
||||
value?: any;
|
||||
@property(() => String)
|
||||
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: 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
const data = decoverto.type(ResourceFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
await data.push(workspace, remotePath);
|
||||
}
|
||||
|
||||
type PushOptions = GlobalOptions;
|
||||
|
||||
@@ -2,20 +2,52 @@
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import {
|
||||
colors,
|
||||
Command,
|
||||
JobService,
|
||||
readAll,
|
||||
Script,
|
||||
} from "https://deno.land/x/windmill@v1.50.0/windmill-api/index.ts";
|
||||
import { colors, Command, readAll, ScriptService, Table } from "./deps.ts";
|
||||
ScriptService,
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, array, decoverto, model, property } from "./decoverto.ts";
|
||||
|
||||
type ScriptFile = {
|
||||
@model()
|
||||
export class ScriptFile {
|
||||
@property(() => String)
|
||||
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(
|
||||
@@ -48,10 +80,12 @@ 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(
|
||||
@@ -79,23 +113,35 @@ 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: ScriptFile = JSON.parse(await Deno.readTextFile(filePath));
|
||||
const data = decoverto.type(ScriptFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
const content = await Deno.readTextFile(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);
|
||||
}
|
||||
|
||||
const language = inferContentTypeFromFilePath(contentPath);
|
||||
let parent_hash = data.parent_hash;
|
||||
if (!parent_hash) {
|
||||
try {
|
||||
|
||||
969
cli/sync.ts
Normal file
969
cli/sync.ts
Normal file
@@ -0,0 +1,969 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"workspace_id": "admins",
|
||||
"name": "my_folder",
|
||||
"display_name": "my_folder",
|
||||
"owners": [],
|
||||
"extra_perms": {
|
||||
"u/test": true,
|
||||
"u/admin@windmill.dev": false
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
||||
133
cli/types.ts
133
cli/types.ts
@@ -1,4 +1,137 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
180
cli/variable.ts
180
cli/variable.ts
@@ -1,7 +1,22 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
import { colors, Command, Table, VariableService } from "./deps.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";
|
||||
|
||||
async function list(opts: GlobalOptions) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
@@ -26,13 +41,100 @@ async function list(opts: GlobalOptions) {
|
||||
.render();
|
||||
}
|
||||
|
||||
type VariableFile = {
|
||||
@model()
|
||||
export class VariableFile implements Resource, PushDiffs {
|
||||
@property(() => String)
|
||||
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);
|
||||
@@ -58,72 +160,10 @@ export async function pushVariable(
|
||||
filePath: string,
|
||||
remotePath: string,
|
||||
) {
|
||||
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 data = decoverto.type(VariableFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
await data.push(workspace, remotePath);
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
|
||||
@@ -11,14 +11,31 @@ import {
|
||||
Table,
|
||||
WorkspaceService,
|
||||
} from "./deps.ts";
|
||||
import { requireLogin } from "./context.ts";
|
||||
import { decoverto, model, property } from "./decoverto.ts";
|
||||
|
||||
export type Workspace = {
|
||||
@model()
|
||||
export class Workspace {
|
||||
@property(() => String)
|
||||
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>,
|
||||
@@ -33,7 +50,9 @@ function makeWorkspaceStream(
|
||||
if (line.length <= 2) {
|
||||
return;
|
||||
}
|
||||
controller.enqueue(JSON.parse(line) as Workspace);
|
||||
const workspace = decoverto.type(Workspace).rawToInstance(line);
|
||||
workspace.remote = new URL(workspace.remote).toString(); // add trailing slash in all cases!
|
||||
controller.enqueue(workspace);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -42,14 +61,17 @@ function makeWorkspaceStream(
|
||||
);
|
||||
}
|
||||
|
||||
async function allWorkspaces(): Promise<Workspace[]> {
|
||||
try {
|
||||
const file = await Deno.open((await getRootStore()) + "remotes.ndjson", {
|
||||
write: false,
|
||||
read: true,
|
||||
});
|
||||
const workspaceStream = makeWorkspaceStream(file.readable);
|
||||
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[]> {
|
||||
try {
|
||||
const workspaceStream = await getWorkspaceStream();
|
||||
const workspaces: Workspace[] = [];
|
||||
for await (const workspace of workspaceStream) {
|
||||
workspaces.push(workspace);
|
||||
@@ -87,8 +109,7 @@ export async function getActiveWorkspace(
|
||||
export async function getWorkspaceByName(
|
||||
workspaceName: string,
|
||||
): Promise<Workspace | undefined> {
|
||||
const file = await Deno.open((await getRootStore()) + "remotes.ndjson");
|
||||
const workspaceStream = makeWorkspaceStream(file.readable);
|
||||
const workspaceStream = await getWorkspaceStream();
|
||||
for await (const workspace of workspaceStream) {
|
||||
if (workspace.name === workspaceName) {
|
||||
return workspace;
|
||||
@@ -185,6 +206,7 @@ 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) {
|
||||
@@ -192,7 +214,10 @@ 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({
|
||||
@@ -224,6 +249,7 @@ 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,
|
||||
@@ -234,7 +260,7 @@ export async function addWorkspace(workspace: Workspace) {
|
||||
file.close();
|
||||
}
|
||||
|
||||
async function remove(_opts: GlobalOptions, name: string) {
|
||||
export async function removeWorkspace(name: string) {
|
||||
const orgWorkspaces = await allWorkspaces();
|
||||
await Deno.writeTextFile(
|
||||
(await getRootStore()) + "remotes.ndjson",
|
||||
@@ -243,6 +269,10 @@ async function remove(_opts: GlobalOptions, 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!"));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,29 +20,41 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
windmill:
|
||||
windmill_server:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
# Set privileged to true if enabling nsjail
|
||||
privileged: false
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||
- BASE_URL=http://${WM_BASE_URL}
|
||||
- RUST_LOG=info
|
||||
## You can set the number of workers to > 0 and not need any separate worker service
|
||||
- NUM_WORKERS=0
|
||||
- DISABLE_SERVER=false
|
||||
- METRICS_ADDR=false
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
windmill_worker:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
deploy:
|
||||
replicas: 3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||
- BASE_URL=http://${WM_BASE_URL}
|
||||
- BASE_INTERNAL_URL=http://localhost:8000
|
||||
- RUST_LOG=info
|
||||
- NUM_WORKERS=3
|
||||
- NUM_WORKERS=1
|
||||
- DISABLE_SERVER=true
|
||||
- KEEP_JOB_DIR=false
|
||||
- DENO_PATH=/usr/bin/deno
|
||||
- PYTHON_PATH=/usr/local/bin/python3
|
||||
- METRICS_ADDR=false
|
||||
# for ease of use, nsjail which provide isolation in untrusted environment is disabled by default.
|
||||
# To enable it, uncomment the following line, set the container as privileged and
|
||||
# rebuild the image with nsjail=true or use the enterprise edition
|
||||
# - DISABLE_NSJAIL=false
|
||||
# - DISABLE_NUSER=false
|
||||
# - NSJAIL_PATH=nsjail
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/dist
|
||||
/.svelte-kit
|
||||
/package
|
||||
/src/lib/gen
|
||||
|
||||
5
frontend/embed/README.md
Normal file
5
frontend/embed/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Build the app preview embed code
|
||||
|
||||
./embed/build.sh
|
||||
|
||||
serve dist
|
||||
6
frontend/embed/build.sh
Executable file
6
frontend/embed/build.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
mkdir -p dist
|
||||
npm run build-embed
|
||||
cp static/Inter-Variable.ttf dist
|
||||
cp static/tailwind.js dist
|
||||
npx tailwindcss -o src/lib/assets/app.css --minify -o dist/tailwind.css
|
||||
cp embed/index.html dist
|
||||
840
frontend/embed/index.html
Normal file
840
frontend/embed/index.html
Normal file
@@ -0,0 +1,840 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Embed demo</title>
|
||||
<link href="/style.css" rel="stylesheet">
|
||||
<link href="/tailwind.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/appviewer.iife.js">
|
||||
</script>
|
||||
</script>
|
||||
<script>
|
||||
loadApp(document.getElementById("app"), {
|
||||
"grid": [
|
||||
{
|
||||
"3": {
|
||||
"h": 2,
|
||||
"w": 1,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 5,
|
||||
"w": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"id": "a",
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "a",
|
||||
"data": {
|
||||
"id": "a",
|
||||
"card": false,
|
||||
"type": "htmlcomponent",
|
||||
"softWrap": false,
|
||||
"configuration": {},
|
||||
"componentInput": {
|
||||
"eval": "<img\n\tsrc=\"https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1024&h=1280&q=80\"\n>\n<div class=\"absolute top-4 left-2 text-white\">\n\tHello ${ctx.username}\n</div>\t\t\t\n",
|
||||
"type": "template",
|
||||
"value": "<img\n\tsrc=\"https://images.unsplash.com/photo-1554629947-334ff61d85dc?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1024&h=1280&q=80\"\n>\n<div class=\"absolute top-4 left-2 text-white\">\n\tHello ${ctx.username}\n</div>\t\t\t\n",
|
||||
"fieldType": "template"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 10,
|
||||
"w": 3,
|
||||
"x": 0,
|
||||
"y": 2,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 10,
|
||||
"w": 6,
|
||||
"x": 3,
|
||||
"y": 0,
|
||||
"id": "b",
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "b",
|
||||
"data": {
|
||||
"id": "b",
|
||||
"card": true,
|
||||
"type": "tablecomponent",
|
||||
"actionButtons": [],
|
||||
"configuration": {
|
||||
"search": {
|
||||
"type": "static",
|
||||
"value": "Disabled",
|
||||
"fieldType": "select",
|
||||
"onlyStatic": true,
|
||||
"optionValuesKey": "tableSearchOptions"
|
||||
}
|
||||
},
|
||||
"componentInput": {
|
||||
"type": "static",
|
||||
"value": [
|
||||
{
|
||||
"id": 1,
|
||||
"age": 42,
|
||||
"name": "A cell with a long name"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"age": 84,
|
||||
"name": "A briefer cell"
|
||||
}
|
||||
],
|
||||
"fieldType": "array",
|
||||
"subFieldType": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 8,
|
||||
"w": 2,
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 8,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "c",
|
||||
"data": {
|
||||
"id": "c",
|
||||
"card": true,
|
||||
"type": "scatterchartcomponent",
|
||||
"configuration": {
|
||||
"pannable": {
|
||||
"type": "static",
|
||||
"value": false,
|
||||
"fieldType": "boolean",
|
||||
"onlyStatic": true
|
||||
},
|
||||
"zoomable": {
|
||||
"type": "static",
|
||||
"value": false,
|
||||
"fieldType": "boolean",
|
||||
"onlyStatic": true
|
||||
}
|
||||
},
|
||||
"componentInput": {
|
||||
"type": "static",
|
||||
"value": [
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"x": 25,
|
||||
"y": 50
|
||||
},
|
||||
{
|
||||
"x": 23,
|
||||
"y": 23
|
||||
},
|
||||
{
|
||||
"x": 12,
|
||||
"y": 37
|
||||
}
|
||||
],
|
||||
"label": "foo",
|
||||
"backgroundColor": "rgb(255, 12, 137)"
|
||||
},
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
{
|
||||
"x": 25,
|
||||
"y": 42
|
||||
},
|
||||
{
|
||||
"x": 3,
|
||||
"y": 27
|
||||
}
|
||||
],
|
||||
"label": "foobar",
|
||||
"backgroundColor": "orange"
|
||||
}
|
||||
],
|
||||
"fieldType": "array",
|
||||
"subFieldType": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 8,
|
||||
"w": 2,
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 8,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 10,
|
||||
"id": "d",
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "d",
|
||||
"data": {
|
||||
"id": "d",
|
||||
"card": false,
|
||||
"type": "displaycomponent",
|
||||
"configuration": {},
|
||||
"componentInput": {
|
||||
"type": "connected",
|
||||
"value": {
|
||||
"foo": 42
|
||||
},
|
||||
"fields": {
|
||||
"x": {
|
||||
"type": "connected",
|
||||
"value": "gooo",
|
||||
"visible": true,
|
||||
"fieldType": "string",
|
||||
"connection": {
|
||||
"path": "result",
|
||||
"componentId": "e"
|
||||
}
|
||||
}
|
||||
},
|
||||
"runnable": {
|
||||
"name": "Inline Script 1",
|
||||
"type": "runnableByName",
|
||||
"inlineScript": {
|
||||
"path": "/inline-script/Inline Script 1",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [
|
||||
"x"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": "// import * as wmill from \"https://deno.land/x/windmill@v1.57.1/mod.ts\"\n\nexport async function main(x: string) {\n return x\n}\n",
|
||||
"language": "deno"
|
||||
}
|
||||
},
|
||||
"fieldType": "object",
|
||||
"connection": {
|
||||
"path": "result",
|
||||
"componentId": "e"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 1,
|
||||
"w": 2,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 1,
|
||||
"w": 3,
|
||||
"x": 9,
|
||||
"y": 0,
|
||||
"id": "e",
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "e",
|
||||
"data": {
|
||||
"id": "e",
|
||||
"card": false,
|
||||
"type": "textinputcomponent",
|
||||
"softWrap": true,
|
||||
"configuration": {
|
||||
"placeholder": {
|
||||
"type": "static",
|
||||
"value": "Type...",
|
||||
"fieldType": "text",
|
||||
"onlyStatic": true
|
||||
}
|
||||
},
|
||||
"verticalAlignment": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 1,
|
||||
"w": 2,
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 1,
|
||||
"w": 3,
|
||||
"x": 9,
|
||||
"y": 1,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "f",
|
||||
"data": {
|
||||
"id": "f",
|
||||
"card": false,
|
||||
"type": "passwordinputcomponent",
|
||||
"softWrap": true,
|
||||
"configuration": {
|
||||
"placeholder": {
|
||||
"type": "static",
|
||||
"value": "Password",
|
||||
"fieldType": "text",
|
||||
"onlyStatic": true
|
||||
}
|
||||
},
|
||||
"verticalAlignment": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 1,
|
||||
"w": 2,
|
||||
"x": 0,
|
||||
"y": 28,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 1,
|
||||
"w": 3,
|
||||
"x": 9,
|
||||
"y": 2,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "g",
|
||||
"data": {
|
||||
"id": "g",
|
||||
"card": false,
|
||||
"type": "dateinputcomponent",
|
||||
"softWrap": true,
|
||||
"configuration": {
|
||||
"maxDate": {
|
||||
"type": "static",
|
||||
"value": "",
|
||||
"fieldType": "date"
|
||||
},
|
||||
"minDate": {
|
||||
"type": "static",
|
||||
"value": "",
|
||||
"fieldType": "date"
|
||||
}
|
||||
},
|
||||
"verticalAlignment": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 2,
|
||||
"y": 12,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 1,
|
||||
"w": 2,
|
||||
"x": 9,
|
||||
"y": 3,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "h",
|
||||
"data": {
|
||||
"id": "h",
|
||||
"card": false,
|
||||
"type": "checkboxcomponent",
|
||||
"softWrap": true,
|
||||
"configuration": {
|
||||
"label": {
|
||||
"type": "static",
|
||||
"value": "Label",
|
||||
"fieldType": "text"
|
||||
}
|
||||
},
|
||||
"verticalAlignment": "center",
|
||||
"horizontalAlignment": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 1,
|
||||
"w": 2,
|
||||
"x": 0,
|
||||
"y": 29,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 1,
|
||||
"w": 3,
|
||||
"x": 9,
|
||||
"y": 4,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "i",
|
||||
"data": {
|
||||
"id": "i",
|
||||
"card": false,
|
||||
"type": "selectcomponent",
|
||||
"softWrap": true,
|
||||
"configuration": {
|
||||
"items": {
|
||||
"type": "static",
|
||||
"value": [
|
||||
{
|
||||
"label": "Foo",
|
||||
"value": "foo"
|
||||
},
|
||||
{
|
||||
"label": "Bar",
|
||||
"value": "bar"
|
||||
}
|
||||
],
|
||||
"fieldType": "array",
|
||||
"subFieldType": "object"
|
||||
},
|
||||
"itemKey": {
|
||||
"type": "static",
|
||||
"value": "value",
|
||||
"fieldType": "text"
|
||||
}
|
||||
},
|
||||
"verticalAlignment": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 1,
|
||||
"w": 1,
|
||||
"x": 2,
|
||||
"y": 13,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 1,
|
||||
"w": 2,
|
||||
"x": 0,
|
||||
"y": 5,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "j",
|
||||
"data": {
|
||||
"id": "j",
|
||||
"card": false,
|
||||
"type": "buttoncomponent",
|
||||
"softWrap": true,
|
||||
"configuration": {
|
||||
"size": {
|
||||
"type": "static",
|
||||
"value": "xs",
|
||||
"fieldType": "select",
|
||||
"onlyStatic": true,
|
||||
"optionValuesKey": "buttonSizeOptions"
|
||||
},
|
||||
"color": {
|
||||
"type": "static",
|
||||
"value": "blue",
|
||||
"fieldType": "select",
|
||||
"onlyStatic": true,
|
||||
"optionValuesKey": "buttonColorOptions"
|
||||
},
|
||||
"label": {
|
||||
"type": "static",
|
||||
"value": "Press me",
|
||||
"fieldType": "text"
|
||||
}
|
||||
},
|
||||
"componentInput": {
|
||||
"type": "runnable",
|
||||
"fields": {},
|
||||
"fieldType": "any"
|
||||
},
|
||||
"verticalAlignment": "center",
|
||||
"horizontalAlignment": "center"
|
||||
}
|
||||
},
|
||||
{
|
||||
"3": {
|
||||
"h": 5,
|
||||
"w": 3,
|
||||
"x": 0,
|
||||
"y": 30,
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 3
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"12": {
|
||||
"h": 5,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 18,
|
||||
"id": "k",
|
||||
"max": {
|
||||
"h": 100,
|
||||
"w": 12
|
||||
},
|
||||
"min": {
|
||||
"h": 1,
|
||||
"w": 1
|
||||
},
|
||||
"fixed": false,
|
||||
"draggable": true,
|
||||
"resizable": true,
|
||||
"customDragger": false,
|
||||
"customResizer": false
|
||||
},
|
||||
"id": "k",
|
||||
"data": {
|
||||
"id": "k",
|
||||
"card": true,
|
||||
"type": "formcomponent",
|
||||
"configuration": {
|
||||
"size": {
|
||||
"type": "static",
|
||||
"value": "xs",
|
||||
"fieldType": "select",
|
||||
"onlyStatic": true,
|
||||
"optionValuesKey": "buttonSizeOptions"
|
||||
},
|
||||
"color": {
|
||||
"type": "static",
|
||||
"value": "dark",
|
||||
"fieldType": "select",
|
||||
"onlyStatic": true,
|
||||
"optionValuesKey": "buttonColorOptions"
|
||||
},
|
||||
"label": {
|
||||
"type": "static",
|
||||
"value": "Submit",
|
||||
"fieldType": "text"
|
||||
}
|
||||
},
|
||||
"componentInput": {
|
||||
"type": "runnable",
|
||||
"fields": {
|
||||
"x": {
|
||||
"type": "user",
|
||||
"value": null,
|
||||
"visible": true,
|
||||
"fieldType": "string"
|
||||
},
|
||||
"y": {
|
||||
"type": "user",
|
||||
"value": null,
|
||||
"visible": true,
|
||||
"fieldType": "string"
|
||||
}
|
||||
},
|
||||
"runnable": {
|
||||
"name": "Inline Script 0",
|
||||
"type": "runnableByName",
|
||||
"inlineScript": {
|
||||
"path": "/inline-script/Inline Script 0",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": ""
|
||||
},
|
||||
"y": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"content": "// import * as wmill from \"https://deno.land/x/windmill@v1.57.1/mod.ts\"\n\nexport async function main(x: string, y: string) {\n return x\n}\n",
|
||||
"language": "deno"
|
||||
}
|
||||
},
|
||||
"fieldType": "any"
|
||||
},
|
||||
"horizontalAlignment": "center"
|
||||
}
|
||||
}
|
||||
],
|
||||
"fullscreen": false,
|
||||
"unusedInlineScripts": []
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
820
frontend/package-lock.json
generated
820
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "windmill",
|
||||
"version": "1.61.1",
|
||||
"version": "1.62.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build-embed": "vite build -c vite.embed.config.js",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json --threshold warning",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
@@ -16,9 +17,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@rollup/plugin-commonjs": "^24.0.1",
|
||||
"@rollup/plugin-html": "^1.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@sveltejs/adapter-static": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.0-next.589",
|
||||
"@sveltejs/package": "^1.0.1",
|
||||
"@sveltejs/package": "^1.0.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -34,15 +40,17 @@
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"lucide-svelte": "^0.104.0",
|
||||
"monaco-editor-workers": "0.33.0",
|
||||
"openapi-typescript-codegen": "^0.23.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-svelte": "^2.9.0",
|
||||
"rollup-plugin-svelte": "^7.1.0",
|
||||
"simple-svelte-autocomplete": "^2.5.1",
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
"svelte": "^3.53.1",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-awesome": "^3.0.0",
|
||||
"svelte-check": "^3.0.2",
|
||||
"svelte-dnd-action": "^0.9.22",
|
||||
@@ -51,13 +59,13 @@
|
||||
"svelte-highlight": "^6.2.1",
|
||||
"svelte-overlay": "^1.4.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^5.0.0",
|
||||
"svelte-preprocess": "^5.0.1",
|
||||
"svelte-splitpanes": "^0.7.3",
|
||||
"svelte2tsx": "^0.5.22",
|
||||
"tailwindcss": "^3.2.2",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.0.3"
|
||||
"vite": "^4.0.4"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -67,6 +75,7 @@
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.7.6",
|
||||
"async-mutex": "^0.4.0",
|
||||
"chart.js": "3.8.2",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-zoom": "^2.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
@@ -83,5 +92,6 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.53.1"
|
||||
}
|
||||
},
|
||||
"main": "index.js"
|
||||
}
|
||||
|
||||
9
frontend/src/embed.js
Normal file
9
frontend/src/embed.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import AppPreview from './lib/components/apps/editor/AppPreview.svelte'
|
||||
|
||||
globalThis.process = { env: { NODE_ENV: 'production' } }
|
||||
globalThis.loadApp = (target, value) => {
|
||||
new AppPreview({
|
||||
target: target,
|
||||
props: { app: value }
|
||||
})
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { truncate } from '$lib/utils'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import Drawer from './common/drawer/Drawer.svelte'
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
|
||||
{/if}
|
||||
{#if editableSchema}
|
||||
<div class="my-1 text-xs border-solid border border-gray-400 rounded p-2">
|
||||
<div class="p-2 my-1 text-xs border-solid border border-gray-400">
|
||||
<span
|
||||
class="underline"
|
||||
on:click={() => {
|
||||
|
||||
51
frontend/src/lib/components/CliHelpBox.svelte
Normal file
51
frontend/src/lib/components/CliHelpBox.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<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}
|
||||
@@ -20,6 +20,7 @@
|
||||
| 'gif'
|
||||
| 'error'
|
||||
| 'approval'
|
||||
| 'svg'
|
||||
| undefined
|
||||
|
||||
$: resultKind = inferResultKind(result)
|
||||
@@ -61,6 +62,8 @@
|
||||
return 'html'
|
||||
} else if (keys.length == 1 && keys[0] == 'png') {
|
||||
return 'png'
|
||||
} else if (keys.length == 1 && keys[0] == 'svg') {
|
||||
return 'svg'
|
||||
} else if (keys.length == 1 && keys[0] == 'jpeg') {
|
||||
return 'jpeg'
|
||||
} else if (keys.length == 1 && keys[0] == 'file') {
|
||||
@@ -137,16 +140,16 @@
|
||||
</div>
|
||||
<p
|
||||
class="text-gray-600 mb-2 text-left border-2 !border-t-0 rounded-b border-red-400 overflow-auto p-1"
|
||||
>Rendering HTML can expose you to <a href="https://owasp.org/www-community/attacks/xss/" target="_blank" rel="noreferrer" class="hover:underline">XSS attacks</a>.
|
||||
Only enable it if you trust the author of the script.
|
||||
>Rendering HTML can expose you to <a
|
||||
href="https://owasp.org/www-community/attacks/xss/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="hover:underline">XSS attacks</a
|
||||
>. Only enable it if you trust the author of the script.
|
||||
</p>
|
||||
</div>
|
||||
<div class="center-center">
|
||||
<Button
|
||||
size="sm"
|
||||
color="dark"
|
||||
on:click={() => enableHtml = true}
|
||||
>
|
||||
<Button size="sm" color="dark" on:click={() => (enableHtml = true)}>
|
||||
Enable HTML rendering
|
||||
</Button>
|
||||
</div>
|
||||
@@ -165,6 +168,11 @@ Only enable it if you trust the author of the script.
|
||||
src="data:image/jpeg;base64,{result.jpeg}"
|
||||
/>
|
||||
</div>
|
||||
{:else if !forceJson && resultKind == 'svg'}
|
||||
<div
|
||||
><a download="windmill.svg" href="data:text/plain;base64,{btoa(result.svg)}">Download</a>
|
||||
</div>
|
||||
<div class="h-full overflow-auto">{@html result.svg} </div>
|
||||
{:else if !forceJson && resultKind == 'gif'}
|
||||
<div class="h-full"
|
||||
><img alt="gif rendered" class="w-auto h-full" src="data:image/gif;base64,{result.gif}" />
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||
|
||||
import { buildWorkerDefinition } from 'monaco-editor-workers'
|
||||
import type {
|
||||
Disposable,
|
||||
DocumentUri,
|
||||
@@ -90,13 +89,7 @@
|
||||
const uri = `file:///tmp/monaco/${hash}.${langToExt(lang)}`
|
||||
|
||||
if (browser) {
|
||||
if (dev) {
|
||||
buildWorkerDefinition(
|
||||
'../../../node_modules/monaco-editor-workers/dist/workers',
|
||||
import.meta.url,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
if (!dev) {
|
||||
// @ts-ignore
|
||||
self.MonacoEnvironment = {
|
||||
getWorker: function (_moduleId: any, label: string) {
|
||||
@@ -298,8 +291,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
const wsProtocol = $page.url.protocol == 'https:' ? 'wss' : 'ws'
|
||||
if (lang == 'typescript') {
|
||||
await connectToLanguageServer(`wss://${$page.url.host}/ws/deno`, 'deno', {
|
||||
await connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/deno`, 'deno', {
|
||||
certificateStores: null,
|
||||
enablePaths: [],
|
||||
config: null,
|
||||
@@ -330,7 +324,7 @@
|
||||
}
|
||||
})
|
||||
} else if (lang === 'python') {
|
||||
await connectToLanguageServer(`wss://${$page.url.host}/ws/pyright`, 'pyright', {
|
||||
await connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/pyright`, 'pyright', {
|
||||
executionEnvironments: [
|
||||
{
|
||||
root: '/tmp/pyright',
|
||||
@@ -341,7 +335,7 @@
|
||||
]
|
||||
})
|
||||
|
||||
connectToLanguageServer(`wss://${$page.url.host}/ws/black`, 'black', {
|
||||
connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/black`, 'black', {
|
||||
formatters: {
|
||||
black: {
|
||||
command: 'black',
|
||||
@@ -353,7 +347,7 @@
|
||||
}
|
||||
})
|
||||
} else if (lang === 'go') {
|
||||
connectToLanguageServer(`wss://${$page.url.host}/ws/go`, 'go', {
|
||||
connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/go`, 'go', {
|
||||
'build.allowImplicitNetworkAccess': true
|
||||
})
|
||||
}
|
||||
@@ -407,6 +401,14 @@
|
||||
}
|
||||
|
||||
async function loadMonaco() {
|
||||
if (dev) {
|
||||
let { buildWorkerDefinition } = await import('monaco-editor-workers')
|
||||
buildWorkerDefinition(
|
||||
'../../../node_modules/monaco-editor-workers/dist/workers',
|
||||
import.meta.url,
|
||||
false
|
||||
)
|
||||
}
|
||||
const model = meditor.createModel(code, lang, mUri.parse(uri))
|
||||
|
||||
model.updateOptions(updateOptions)
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
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
|
||||
@@ -225,121 +227,137 @@
|
||||
<ResourceEditor bind:this={resourceEditor} on:refresh={resourcePicker.openDrawer} />
|
||||
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
|
||||
|
||||
<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>)
|
||||
<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
|
||||
{/if}
|
||||
</span>
|
||||
</Button></div
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
btnClasses="!font-medium"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
color="light"
|
||||
on:click={editor.format}
|
||||
startIcon={{ icon: faBroom }}
|
||||
>
|
||||
{#if !iconOnly}
|
||||
<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 }}
|
||||
>
|
||||
Format (Ctrl+S)
|
||||
{/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>
|
||||
</Button>
|
||||
<svelte:fragment slot="text">
|
||||
Format <Kbd class="!text-gray-800">Ctrl</Kbd> + <Kbd class="!text-gray-800">S</Kbd>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</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>
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
await createSchedule(flow.path)
|
||||
}
|
||||
}
|
||||
sendUserToast(`Success! flow saved at ${$flowStore.path}`)
|
||||
goto(`/flows/get/${$flowStore.path}`)
|
||||
sendUserToast(`Flow saved at ${$flowStore.path}`)
|
||||
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
import type { Job } from '$lib/gen'
|
||||
import JobStatus from '$lib/components/JobStatus.svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { displayDaysAgo } from '$lib/utils'
|
||||
import { displayDate, displayDaysAgo } from '$lib/utils'
|
||||
import {
|
||||
faCalendar,
|
||||
faClock,
|
||||
faRobot,
|
||||
faScroll,
|
||||
faUser,
|
||||
faBarsStaggered
|
||||
faBarsStaggered,
|
||||
faMemory
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import ScheduleEditor from './ScheduleEditor.svelte'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
@@ -39,16 +40,31 @@
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
|
||||
class="mx-2 text-2xs text-gray-600"
|
||||
>
|
||||
{#key time}
|
||||
Received job {displayDaysAgo(job.created_at ?? '')}
|
||||
{/key}</span
|
||||
{#if job['success'] != undefined}
|
||||
Received job: {displayDate(job.created_at ?? '')}
|
||||
{:else}
|
||||
{#key time}
|
||||
Received job {displayDaysAgo(job.created_at ?? '')}
|
||||
{/key}
|
||||
{/if}</span
|
||||
>
|
||||
</div>
|
||||
{#if job && 'started_at' in job && job.started_at}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
{#key time}
|
||||
Started {displayDaysAgo(job.started_at ?? '')}{/key}</span
|
||||
{#if job['success'] != undefined}
|
||||
Started: {displayDate(job.started_at ?? '')}
|
||||
{:else}
|
||||
{#key time}
|
||||
Started {displayDaysAgo(job.started_at ?? '')}{/key}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if job && job['mem_peak']}
|
||||
<div>
|
||||
<Icon class="text-gray-700" data={faMemory} scale={SMALL_ICON_SCALE} /><span class="mx-2">
|
||||
Mem peak: {(job['mem_peak'] / 1024).toPrecision(5)}MB</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -26,4 +26,10 @@
|
||||
$: lang = getLang(language)
|
||||
</script>
|
||||
|
||||
<Highlight className="nowrap {$$props.class}" language={lang} {code} />
|
||||
{#if code?.length < 5000}
|
||||
<Highlight className="nowrap {$$props.class}" language={lang} {code} />
|
||||
{:else}
|
||||
<pre class="overflow-auto max-h-screen {$$props.class}"
|
||||
><code class="language-{language}">{code}</code></pre
|
||||
>
|
||||
{/if}
|
||||
|
||||
13
frontend/src/lib/components/InlineCodeCopy.svelte
Normal file
13
frontend/src/lib/components/InlineCodeCopy.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<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
|
||||
>
|
||||
@@ -8,7 +8,6 @@
|
||||
import DynamicInputHelpBox from './flows/content/DynamicInputHelpBox.svelte'
|
||||
import type { PropPickerWrapperContext } from './flows/propPicker/PropPickerWrapper.svelte'
|
||||
import { codeToStaticTemplate, getDefaultExpr, isCodeInjection } from './flows/utils'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import { Button, ToggleButton, ToggleButtonGroup } from './common'
|
||||
import { faCode } from '@fortawesome/free-solid-svg-icons'
|
||||
import type VariableEditor from './VariableEditor.svelte'
|
||||
@@ -17,6 +16,7 @@
|
||||
import TemplateEditor from './TemplateEditor.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { setInputCat as computeInputCat } from '$lib/utils'
|
||||
import JavascriptEditor from './JavascriptEditor.svelte'
|
||||
|
||||
export let schema: Schema
|
||||
export let arg: InputTransform | any
|
||||
@@ -29,7 +29,7 @@
|
||||
export let itemPicker: ItemPicker | undefined = undefined
|
||||
export let noDynamicToggle = false
|
||||
|
||||
let monaco: SimpleEditor | undefined = undefined
|
||||
let monaco: JavascriptEditor | undefined = undefined
|
||||
let monacoTemplate: TemplateEditor | undefined = undefined
|
||||
let argInput: ArgInput | undefined = undefined
|
||||
|
||||
@@ -284,7 +284,7 @@
|
||||
/>
|
||||
{:else if arg.expr != undefined}
|
||||
<div class="border rounded mt-2 border-gray-300">
|
||||
<SimpleEditor
|
||||
<JavascriptEditor
|
||||
bind:this={monaco}
|
||||
bind:code={arg.expr}
|
||||
{extraLib}
|
||||
|
||||
212
frontend/src/lib/components/JavascriptEditor.svelte
Normal file
212
frontend/src/lib/components/JavascriptEditor.svelte
Normal file
@@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
import { browser, dev } from '$app/environment'
|
||||
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { createHash, editorConfig, langToExt, updateOptions } from '$lib/editorUtils'
|
||||
import { languages, editor as meditor, KeyCode, KeyMod, Uri as mUri } from 'monaco-editor'
|
||||
|
||||
import libStdContent from '$lib/es5.d.ts.txt?raw'
|
||||
|
||||
meditor.defineTheme('myTheme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editorLineNumber.foreground': '#999',
|
||||
'editorGutter.background': '#F9FAFB'
|
||||
}
|
||||
})
|
||||
meditor.setTheme('myTheme')
|
||||
|
||||
languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
noLib: true
|
||||
})
|
||||
|
||||
let divEl: HTMLDivElement | null = null
|
||||
let editor: meditor.IStandaloneCodeEditor
|
||||
let model: meditor.ITextModel
|
||||
|
||||
export let lang: string
|
||||
export let code: string = ''
|
||||
export let hash: string = createHash()
|
||||
export let cmdEnterAction: (() => void) | undefined = undefined
|
||||
export let formatAction: (() => void) | undefined = undefined
|
||||
export let automaticLayout = true
|
||||
export let extraLib: string = ''
|
||||
export let shouldBindKey: boolean = true
|
||||
export let autoHeight = false
|
||||
export let fixedOverflowWidgets = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const uri = `file:///${hash}.${langToExt(lang)}`
|
||||
|
||||
if (browser) {
|
||||
if (!dev) {
|
||||
// @ts-ignore
|
||||
self.MonacoEnvironment = {
|
||||
getWorker: function (_moduleId: any, label: string) {
|
||||
if (label === 'typescript' || label === 'javascript') {
|
||||
return new tsWorker()
|
||||
} else {
|
||||
return new editorWorker()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCode(): string {
|
||||
return editor?.getValue() ?? ''
|
||||
}
|
||||
|
||||
export function insertAtCursor(code: string): void {
|
||||
if (editor) {
|
||||
editor.trigger('keyboard', 'type', { text: code })
|
||||
}
|
||||
}
|
||||
|
||||
export function setCode(ncode: string): void {
|
||||
code = ncode
|
||||
if (editor) {
|
||||
editor.setValue(ncode)
|
||||
}
|
||||
}
|
||||
|
||||
function format() {
|
||||
if (editor) {
|
||||
code = getCode()
|
||||
editor.getAction('editor.action.formatDocument').run()
|
||||
if (formatAction) {
|
||||
formatAction()
|
||||
code = getCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let width = 0
|
||||
async function loadMonaco() {
|
||||
if (dev) {
|
||||
let { buildWorkerDefinition } = await import('monaco-editor-workers')
|
||||
buildWorkerDefinition(
|
||||
'../../../node_modules/monaco-editor-workers/dist/workers',
|
||||
import.meta.url,
|
||||
false
|
||||
)
|
||||
}
|
||||
model = meditor.createModel(code, lang, mUri.parse(uri))
|
||||
|
||||
model.updateOptions(updateOptions)
|
||||
|
||||
editor = meditor.create(
|
||||
divEl as HTMLDivElement,
|
||||
editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets)
|
||||
)
|
||||
|
||||
let timeoutModel: NodeJS.Timeout | undefined = undefined
|
||||
editor.onDidChangeModelContent((event) => {
|
||||
timeoutModel && clearTimeout(timeoutModel)
|
||||
timeoutModel = setTimeout(() => {
|
||||
code = getCode()
|
||||
dispatch('change', { code })
|
||||
}, 200)
|
||||
})
|
||||
|
||||
editor.onDidFocusEditorText(() => {
|
||||
dispatch('focus')
|
||||
|
||||
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, function () {
|
||||
code = getCode()
|
||||
shouldBindKey && format && format()
|
||||
})
|
||||
})
|
||||
|
||||
if (autoHeight) {
|
||||
let ignoreEvent = false
|
||||
const updateHeight = () => {
|
||||
const contentHeight = Math.min(1000, editor.getContentHeight())
|
||||
if (divEl) {
|
||||
divEl.style.height = `${contentHeight}px`
|
||||
}
|
||||
try {
|
||||
ignoreEvent = true
|
||||
editor.layout({ width, height: contentHeight })
|
||||
} finally {
|
||||
ignoreEvent = false
|
||||
}
|
||||
}
|
||||
editor.onDidContentSizeChange(updateHeight)
|
||||
updateHeight()
|
||||
}
|
||||
|
||||
editor.onDidFocusEditorText(() => {
|
||||
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, function () {
|
||||
code = getCode()
|
||||
shouldBindKey && format && format()
|
||||
})
|
||||
|
||||
editor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, function () {
|
||||
code = getCode()
|
||||
shouldBindKey && cmdEnterAction && cmdEnterAction()
|
||||
})
|
||||
dispatch('focus')
|
||||
})
|
||||
|
||||
editor.onDidBlurEditorText(() => {
|
||||
code = getCode()
|
||||
dispatch('blur')
|
||||
})
|
||||
|
||||
if (lang == 'javascript') {
|
||||
const stdLib = { content: libStdContent, filePath: 'es5.d.ts' }
|
||||
if (extraLib != '') {
|
||||
languages.typescript.javascriptDefaults.setExtraLibs([
|
||||
{
|
||||
content: extraLib,
|
||||
filePath: 'windmill.d.ts'
|
||||
},
|
||||
stdLib
|
||||
])
|
||||
} else {
|
||||
languages.typescript.javascriptDefaults.setExtraLibs([stdLib])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
loadMonaco()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
try {
|
||||
model && model.dispose()
|
||||
editor && editor.dispose()
|
||||
} catch (err) {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={divEl} class="{$$props.class} editor" bind:clientWidth={width} />
|
||||
|
||||
<style>
|
||||
.editor {
|
||||
@apply rounded-lg p-0;
|
||||
}
|
||||
|
||||
.small-editor {
|
||||
/* stylelint-disable-next-line unit-allowed-list */
|
||||
height: 26vh;
|
||||
}
|
||||
|
||||
.few-lines-editor {
|
||||
/* stylelint-disable-next-line unit-allowed-list */
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
export let content: string | undefined
|
||||
export let isLoading: boolean
|
||||
export let duration: number | undefined = undefined
|
||||
export let mem: number | undefined = undefined
|
||||
|
||||
let scroll = true
|
||||
let div: HTMLElement | null = null
|
||||
@@ -50,6 +51,11 @@
|
||||
{:else if duration}
|
||||
<span class="absolute text-xs text-gray-500 top-2 left-2">took {duration}ms</span>
|
||||
{/if}
|
||||
{#if mem}
|
||||
<span class="absolute text-xs text-gray-500 top-2 left-36"
|
||||
>mem peak: {(mem / 1024).toPrecision(4)}MB</span
|
||||
>
|
||||
{/if}
|
||||
<pre class="whitespace-pre-wrap break-words bg-gray-50 text-xs w-full p-2"
|
||||
>{#if content}<span>{content}</span>{:else if !isLoading}<span class="text-gray-600"
|
||||
>No logs are available yet</span
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
}
|
||||
|
||||
function resourceToValue() {
|
||||
value = `$res:${path}`
|
||||
if (path) {
|
||||
value = `$res:${path}`
|
||||
} else {
|
||||
value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function isResource() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
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 })
|
||||
@@ -20,7 +21,12 @@
|
||||
const extraOpts = {
|
||||
modifiers: [
|
||||
betterPreventOverflow({ padding: 10 }),
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [8, 8]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
@@ -62,7 +68,7 @@
|
||||
<slot />
|
||||
</button>
|
||||
{/if}
|
||||
{#if showTooltip}
|
||||
{#if showTooltip && !disablePopup}
|
||||
<div
|
||||
use:popperContent={extraOpts}
|
||||
on:mouseenter={open}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
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:
|
||||
| {
|
||||
@@ -55,10 +57,15 @@
|
||||
|
||||
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">
|
||||
@@ -171,7 +178,7 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<Toggle
|
||||
options={{
|
||||
right: `run only visible to you`
|
||||
right: `make run invisible to others`
|
||||
}}
|
||||
bind:checked={invisible_to_owner}
|
||||
/>
|
||||
@@ -200,4 +207,20 @@
|
||||
{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>
|
||||
|
||||
@@ -7,11 +7,9 @@
|
||||
import { slide } from 'svelte/transition'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
import InputTransformForm from './InputTransformForm.svelte'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
export let inputTransform = false
|
||||
export let schema: Schema
|
||||
export let args: Record<string, InputTransform | any> = {}
|
||||
export let disabledArgs: string[] = []
|
||||
@@ -19,16 +17,13 @@
|
||||
|
||||
export let editableSchema = false
|
||||
export let isValid: boolean = true
|
||||
export let extraLib: string = 'missing extraLib'
|
||||
export let autofocus = false
|
||||
export let previousModuleId: string | undefined = undefined
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
export let compact = false
|
||||
export let password: string | undefined = undefined
|
||||
export let noVariablePicker = false
|
||||
export let filter: string[] | undefined = undefined
|
||||
export let noDynamicToggle = false
|
||||
export let flexWrap = false
|
||||
export let noDelete = false
|
||||
|
||||
@@ -47,7 +42,6 @@
|
||||
if (!keys.includes(key)) {
|
||||
delete args[key]
|
||||
delete inputCheck[key]
|
||||
console.log('DELETED', key)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -72,20 +66,7 @@
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if !filter || filter.includes(argName)}
|
||||
<div transition:slide|local>
|
||||
{#if inputTransform}
|
||||
<InputTransformForm
|
||||
{previousModuleId}
|
||||
bind:arg={args[argName]}
|
||||
bind:schema
|
||||
bind:argName
|
||||
bind:inputCheck={inputCheck[argName]}
|
||||
bind:extraLib
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
{noDynamicToggle}
|
||||
/>
|
||||
{:else if typeof args == 'object'}
|
||||
{#if typeof args == 'object'}
|
||||
<ArgInput
|
||||
autofocus={i == 0 && autofocus}
|
||||
label={argName}
|
||||
@@ -126,11 +107,7 @@
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, _) => {
|
||||
if (pickForField) {
|
||||
if (inputTransform) {
|
||||
args[pickForField].value = '$var:' + path
|
||||
} else {
|
||||
args[pickForField] = '$var:' + path
|
||||
}
|
||||
args[pickForField] = '$var:' + path
|
||||
}
|
||||
}}
|
||||
itemName="Variable"
|
||||
|
||||
120
frontend/src/lib/components/SchemaFormTransform.svelte
Normal file
120
frontend/src/lib/components/SchemaFormTransform.svelte
Normal file
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService, type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { Button } from './common'
|
||||
import InputTransformForm from './InputTransformForm.svelte'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
export let schema: Schema
|
||||
export let args: Record<string, InputTransform | any> = {}
|
||||
export let isValid: boolean = true
|
||||
export let extraLib: string = 'missing extraLib'
|
||||
export let previousModuleId: string | undefined = undefined
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
export let noVariablePicker = false
|
||||
export let filter: string[] | undefined = undefined
|
||||
export let noDynamicToggle = false
|
||||
export let flexWrap = false
|
||||
export let noDelete = false
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
$: isValid = allTrue(inputCheck) ?? false
|
||||
|
||||
$: if (args == undefined || typeof args !== 'object') {
|
||||
args = {}
|
||||
}
|
||||
|
||||
function removeExtraKey() {
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (!keys.includes(key)) {
|
||||
delete args[key]
|
||||
delete inputCheck[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let pickForField: string | undefined
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
let keys: string[] = []
|
||||
$: {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
|
||||
keys = lkeys
|
||||
if (!noDelete) {
|
||||
removeExtraKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if !filter || filter.includes(argName)}
|
||||
<div transition:slide|local>
|
||||
<InputTransformForm
|
||||
{previousModuleId}
|
||||
bind:arg={args[argName]}
|
||||
bind:schema
|
||||
bind:argName
|
||||
bind:inputCheck={inputCheck[argName]}
|
||||
bind:extraLib
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
{noDynamicToggle}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-gray-500 text-sm">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !noVariablePicker}
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, _) => {
|
||||
if (pickForField) {
|
||||
args[pickForField].value = '$var:' + path
|
||||
}
|
||||
}}
|
||||
itemName="Variable"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
...x
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
slot="submission"
|
||||
class="flex flex-row-reverse w-full bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
|
||||
>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
variableEditor?.initNew?.()
|
||||
}}
|
||||
>
|
||||
New variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
{/if}
|
||||
@@ -14,7 +14,7 @@
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import { Button } from './common'
|
||||
import { Button, Kbd } from './common'
|
||||
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
@@ -83,8 +83,9 @@
|
||||
kind: script.kind
|
||||
}
|
||||
})
|
||||
sendUserToast(`Success! New script version created with hash ${newHash}`)
|
||||
goto(`/scripts/get/${newHash}`)
|
||||
sendUserToast(`New script created at hash ${newHash}`)
|
||||
await goto(`/scripts/edit/${newHash}?step=2`)
|
||||
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
|
||||
} catch (error) {
|
||||
sendUserToast(`Impossible to save the script: ${error.body}`, true)
|
||||
}
|
||||
@@ -103,9 +104,18 @@
|
||||
}
|
||||
goto(`?step=${step}`)
|
||||
}
|
||||
|
||||
$: kind = script.kind as 'script' | 'trigger' | 'approval' | undefined
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key == 'Enter' && step == 1) {
|
||||
changeStep(2)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<UnsavedConfirmationModal />
|
||||
<div class="flex flex-col h-screen">
|
||||
@@ -167,11 +177,11 @@
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step == 1 ? 'contained' : 'border'}
|
||||
btnClasses={step == 3 ? 'invisible' : ''}
|
||||
btnClasses={step == 3 ? 'invisible' : 'inline-flex gap-2'}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
on:click={() => changeStep(step + 1)}
|
||||
>
|
||||
Next
|
||||
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -188,7 +198,7 @@
|
||||
<!-- metadata -->
|
||||
{#if step === 1}
|
||||
<CenteredPage>
|
||||
<h2 class="border-b pb-1 mt-4 mb-2">Path</h2>
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Path</h2>
|
||||
<Path
|
||||
bind:this={pathC}
|
||||
bind:error={pathError}
|
||||
@@ -198,14 +208,14 @@
|
||||
namePlaceholder="my_script"
|
||||
kind="script"
|
||||
/>
|
||||
<h2 class="border-b pb-1 mt-8 mb-4">Summary</h2>
|
||||
<h2 class="border-b pb-1 mt-12 mb-4">Summary</h2>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={summaryC}
|
||||
bind:value={script.summary}
|
||||
placeholder="A short summary of the script displayed when the script is listed"
|
||||
/>
|
||||
<h2 class="border-b pb-1 mt-8 mb-6">Language</h2>
|
||||
<h2 class="border-b pb-1 mt-12 mb-6">Language</h2>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
{#each langs as [label, lang]}
|
||||
{@const isPicked = script.language == lang && template == 'script'}
|
||||
@@ -220,7 +230,7 @@
|
||||
initContent(lang, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
|
||||
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</span>
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
@@ -234,7 +244,7 @@
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">PostgreSQL</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -247,11 +257,10 @@
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2">MySQL</span>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2 py-4">MySQL</span>
|
||||
</Button>
|
||||
</div>
|
||||
<h2 class="border-b pb-1 mt-8 mb-4">Advanced</h2>
|
||||
<div class="mb-4">
|
||||
<div class="mt-16 mb-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
@@ -301,12 +310,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
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
|
||||
import yamlWorker from 'monaco-yaml/yaml.worker?worker'
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||
|
||||
import { buildWorkerDefinition } from 'monaco-editor-workers'
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { createHash, editorConfig, langToExt, updateOptions } from '$lib/editorUtils'
|
||||
import { languages, editor as meditor, KeyCode, KeyMod, Uri as mUri } from 'monaco-editor'
|
||||
|
||||
import libStdContent from '$lib/es5.d.ts.txt?raw'
|
||||
import {
|
||||
languages,
|
||||
editor as meditor,
|
||||
KeyCode,
|
||||
KeyMod,
|
||||
Uri as mUri
|
||||
} from 'monaco-editor/esm/vs/editor/editor.main'
|
||||
|
||||
meditor.defineTheme('myTheme', {
|
||||
base: 'vs',
|
||||
@@ -25,12 +26,6 @@
|
||||
})
|
||||
meditor.setTheme('myTheme')
|
||||
|
||||
languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: languages.typescript.ScriptTarget.Latest,
|
||||
allowNonTsExtensions: true,
|
||||
noLib: true
|
||||
})
|
||||
|
||||
languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
allowComments: false,
|
||||
@@ -48,7 +43,6 @@
|
||||
export let cmdEnterAction: (() => void) | undefined = undefined
|
||||
export let formatAction: (() => void) | undefined = undefined
|
||||
export let automaticLayout = true
|
||||
export let extraLib: string = ''
|
||||
export let shouldBindKey: boolean = true
|
||||
export let autoHeight = false
|
||||
export let fixedOverflowWidgets = true
|
||||
@@ -58,13 +52,7 @@
|
||||
const uri = `file:///${hash}.${langToExt(lang)}`
|
||||
|
||||
if (browser) {
|
||||
if (dev) {
|
||||
buildWorkerDefinition(
|
||||
'../../../node_modules/monaco-editor-workers/dist/workers',
|
||||
import.meta.url,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
if (!dev) {
|
||||
// @ts-ignore
|
||||
self.MonacoEnvironment = {
|
||||
getWorker: function (_moduleId: any, label: string) {
|
||||
@@ -72,8 +60,6 @@
|
||||
return new jsonWorker()
|
||||
} else if (label === 'yaml') {
|
||||
return new yamlWorker()
|
||||
} else if (label === 'typescript' || label === 'javascript') {
|
||||
return new tsWorker()
|
||||
} else {
|
||||
return new editorWorker()
|
||||
}
|
||||
@@ -174,21 +160,6 @@
|
||||
code = getCode()
|
||||
dispatch('blur')
|
||||
})
|
||||
|
||||
if (lang == 'javascript') {
|
||||
const stdLib = { content: libStdContent, filePath: 'es5.d.ts' }
|
||||
if (extraLib != '') {
|
||||
languages.typescript.javascriptDefaults.setExtraLibs([
|
||||
{
|
||||
content: extraLib,
|
||||
filePath: 'windmill.d.ts'
|
||||
},
|
||||
stdLib
|
||||
])
|
||||
} else {
|
||||
languages.typescript.javascriptDefaults.setExtraLibs([stdLib])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
['None', 'none'],
|
||||
['File (base64)', 'base64'],
|
||||
['Enum', 'enum'],
|
||||
['Resource Path', 'resource'],
|
||||
['Format', 'format'],
|
||||
['Pattern', 'pattern']
|
||||
]}
|
||||
|
||||
@@ -109,12 +109,10 @@
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
console.debug('cancelled')
|
||||
}
|
||||
|
||||
export async function clearCurrentJob() {
|
||||
if (currentId) {
|
||||
console.debug('clear')
|
||||
job = undefined
|
||||
await cancelJob()
|
||||
}
|
||||
@@ -148,6 +146,9 @@
|
||||
if (previewJobUpdates.new_logs) {
|
||||
job.logs = (job?.logs ?? '').concat(previewJobUpdates.new_logs)
|
||||
}
|
||||
if (previewJobUpdates.mem_peak) {
|
||||
job.mem_peak = previewJobUpdates.mem_peak
|
||||
}
|
||||
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
|
||||
job = await JobService.getJob({ workspace: workspace!, id })
|
||||
}
|
||||
@@ -180,7 +181,6 @@
|
||||
|
||||
async function syncer(id: string): Promise<void> {
|
||||
if (currentId != id) {
|
||||
console.debug('stop')
|
||||
return
|
||||
}
|
||||
syncIteration++
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let options: {
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
>
|
||||
<div>
|
||||
Added token: <button on:click={() => copyToClipboard(newToken ?? '')} class="inline"
|
||||
>{truncate(newToken ?? '', 10, '****')} <Icon data={faClipboard} />
|
||||
>{newToken} <Icon data={faClipboard} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="pt-1 text-xs ml-2">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
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
|
||||
@@ -14,7 +15,7 @@
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
</script>
|
||||
|
||||
<RunnableWrapper flexWrap bind:result bind:componentInput {id}>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
|
||||
<div class="w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm">
|
||||
Results
|
||||
</div>
|
||||
@@ -18,6 +18,7 @@
|
||||
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export let noWFull = false
|
||||
export let preclickAction: (() => Promise<void>) | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
@@ -28,6 +29,7 @@
|
||||
let size: ButtonType.Size
|
||||
let runnableComponent: RunnableComponent
|
||||
let disabled: boolean | undefined = undefined
|
||||
let fillContainer: boolean | undefined = undefined
|
||||
|
||||
let isLoading: boolean = false
|
||||
let ownClick: boolean = false
|
||||
@@ -67,6 +69,7 @@
|
||||
bind:value={disabled}
|
||||
bind:error={errors.disabled}
|
||||
/>
|
||||
<InputValue {id} input={configuration.fillContainer} bind:value={fillContainer} />
|
||||
|
||||
<RunnableWrapper
|
||||
flexWrap
|
||||
@@ -81,12 +84,16 @@
|
||||
<div class="text-red-500 text-xs">{errorsMessage}</div>
|
||||
{/if}
|
||||
<Button
|
||||
btnClasses={fillContainer ? 'w-full h-full' : ''}
|
||||
{disabled}
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
on:click={async (e) => {
|
||||
if (preclickAction) {
|
||||
await preclickAction()
|
||||
}
|
||||
e?.stopPropagation()
|
||||
e?.preventDefault()
|
||||
ownClick = true
|
||||
@@ -102,7 +109,9 @@
|
||||
{color}
|
||||
{loading}
|
||||
>
|
||||
{labelValue}
|
||||
<span class="truncate">
|
||||
{labelValue}
|
||||
</span>
|
||||
</Button>
|
||||
</AlignWrapper>
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Bar, Line } from 'svelte-chartjs'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
@@ -12,7 +11,6 @@
|
||||
CategoryScale,
|
||||
BarElement
|
||||
} from 'chart.js'
|
||||
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -20,6 +18,7 @@
|
||||
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']
|
||||
|
||||
@@ -71,7 +70,7 @@
|
||||
<InputValue {id} input={configuration.theme} bind:value={theme} />
|
||||
<InputValue {id} input={configuration.line} bind:value={lineChart} />
|
||||
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
|
||||
{#if result}
|
||||
{#if lineChart}
|
||||
<Line {data} {options} />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let initializing: boolean | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
|
||||
@@ -20,14 +21,16 @@
|
||||
bind:clientHeight={h}
|
||||
bind:clientWidth={w}
|
||||
>
|
||||
<RunnableWrapper autoRefresh flexWrap bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper autoRefresh flexWrap bind:componentInput {id} bind:initializing bind:result>
|
||||
{#key result}
|
||||
<iframe
|
||||
frameborder="0"
|
||||
style="height: {h}px; width: {w}px"
|
||||
class="p-0"
|
||||
title="sandbox"
|
||||
srcdoc={result ? '<scr' + `ipt src="/tailwind.css"></script>` + result : 'No html'}
|
||||
srcdoc={result
|
||||
? '<scr' + `ipt type="application/javascript" src="/tailwind.js"></script>` + result
|
||||
: 'No html'}
|
||||
/>
|
||||
{/key}
|
||||
</RunnableWrapper>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Doughnut, Pie } from 'svelte-chartjs'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
@@ -19,6 +18,7 @@
|
||||
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:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
|
||||
{#if result}
|
||||
{#if doughnut}
|
||||
<Doughnut {data} {options} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
@@ -12,7 +11,6 @@
|
||||
CategoryScale,
|
||||
BarElement
|
||||
} from 'chart.js'
|
||||
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import Scatter from 'svelte-chartjs/Scatter.svelte'
|
||||
@@ -21,6 +19,7 @@
|
||||
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
|
||||
@@ -70,7 +69,7 @@
|
||||
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
|
||||
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
|
||||
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
|
||||
{#if result}
|
||||
<Scatter {data} {options} />
|
||||
{/if}
|
||||
|
||||
@@ -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,6 +13,7 @@ import { copyToClipboard } from '../../../../utils'
|
||||
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']
|
||||
|
||||
@@ -59,33 +60,23 @@ import { copyToClipboard } from '../../../../utils'
|
||||
<InputValue {id} input={configuration.style} bind:value={style} />
|
||||
<InputValue {id} input={configuration.copyButton} bind:value={copyButton} />
|
||||
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing 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">
|
||||
No text
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<svelte:element
|
||||
this={component}
|
||||
class="whitespace-pre-wrap {classes}"
|
||||
style={extraStyle}
|
||||
>
|
||||
<div class="flex flex-wrap gap-2 pb-0.5 overflow-x-auto">
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
||||
import 'chartjs-adapter-date-fns'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
@@ -15,7 +14,6 @@
|
||||
TimeScale,
|
||||
LogarithmicScale
|
||||
} from 'chart.js'
|
||||
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import Scatter from 'svelte-chartjs/Scatter.svelte'
|
||||
@@ -24,6 +22,7 @@
|
||||
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']
|
||||
|
||||
@@ -85,7 +84,7 @@
|
||||
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
|
||||
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
|
||||
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap autoRefresh bind:componentInput {id} bind:initializing bind:result>
|
||||
{#if result}
|
||||
<Scatter {data} {options} />
|
||||
{/if}
|
||||
|
||||
@@ -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:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
|
||||
{#if !Plotly}
|
||||
<div class="p-2">
|
||||
<Loader2 class="animate-spin" />
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
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']
|
||||
|
||||
@@ -52,7 +53,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:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing bind:result>
|
||||
{#if !vegaEmbed}
|
||||
<div class="p-2">
|
||||
<Loader2 class="animate-spin" />
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
export let defaultUserInput = false
|
||||
export let flexWrap = false
|
||||
export let wrapperClass = ''
|
||||
export let initializing: boolean | undefined = undefined
|
||||
|
||||
const {
|
||||
worldStore,
|
||||
@@ -305,7 +306,7 @@
|
||||
/>
|
||||
|
||||
<div class="h-full flex relative flex-row flex-wrap {wrapperClass}">
|
||||
{#if autoRefresh === true}
|
||||
{#if !initializing && autoRefresh === true}
|
||||
<div class="flex absolute top-1 right-1">
|
||||
<RefreshButton componentId={id} />
|
||||
</div>
|
||||
@@ -341,7 +342,7 @@
|
||||
<slot />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="block grow max-h-full">
|
||||
<div class="block grow w-full max-h-full">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
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
|
||||
@@ -20,6 +21,10 @@
|
||||
|
||||
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: if (initializing && result) {
|
||||
initializing = false
|
||||
}
|
||||
|
||||
if (noBackend && componentInput?.type == 'runnable') {
|
||||
result = componentInput?.['value']
|
||||
}
|
||||
@@ -46,6 +51,7 @@
|
||||
{id}
|
||||
{extraQueryParams}
|
||||
{forceSchemaDisplay}
|
||||
{initializing}
|
||||
wrapperClass={runnableClass}
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
export const staticOutputs: string[] = ['result']
|
||||
|
||||
let defaultValue: boolean | undefined = undefined
|
||||
let labelValue: string = 'Default label'
|
||||
let value: boolean = false
|
||||
|
||||
// As the checkbox is a special case and has no input
|
||||
// we need to manually set the output
|
||||
@@ -25,9 +25,12 @@
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
result: Output<boolean>
|
||||
}
|
||||
|
||||
$: defaultValue != undefined && outputs.result.set(defaultValue)
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.label} bind:value={labelValue} />
|
||||
<InputValue {id} input={configuration.defaultValue} bind:value={defaultValue} />
|
||||
|
||||
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
|
||||
<Toggle
|
||||
@@ -35,7 +38,7 @@
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
bind:value
|
||||
checked={defaultValue}
|
||||
options={{ right: labelValue }}
|
||||
on:change={(e) => {
|
||||
outputs.result.set(e.detail)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, BaseAppComponent, ButtonComponent } from '../../types'
|
||||
import type { AppEditorContext, BaseAppComponent } from '../../types'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
@@ -13,11 +13,13 @@
|
||||
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',
|
||||
@@ -135,7 +137,7 @@
|
||||
|
||||
<InputValue {id} input={configuration.search} bind:value={search} />
|
||||
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:result>
|
||||
<RunnableWrapper flexWrap bind:componentInput {id} bind:initializing 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'}
|
||||
@@ -210,21 +212,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}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row, rowIndex)
|
||||
}}
|
||||
extraQueryParams={{ row: row.original }}
|
||||
bind:componentInput={actionButton.componentInput}
|
||||
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { AppComponent } from '../types'
|
||||
import { displayData } from '../utils'
|
||||
import { components, type AppComponent } from './Component.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
@@ -8,10 +7,10 @@
|
||||
</script>
|
||||
|
||||
{#if component?.componentInput?.type === 'runnable' && Object.keys(component?.componentInput?.fields ?? {}).length > 0}
|
||||
<div class="mb-8">
|
||||
<div class="mb-8 border p-2">
|
||||
<div class="flex justify-between mb-4">
|
||||
<span class="text-sm font-bold">{component.id}</span>
|
||||
<span class="text-sm font-bold">{displayData[component.type].name}</span>
|
||||
<span class="text-sm font-bold">{components[component.type].name}</span>
|
||||
</div>
|
||||
|
||||
{#if resourceOnly && Object.keys(component.componentInput.fields).filter((fieldKey) => {
|
||||
@@ -22,7 +21,7 @@
|
||||
}
|
||||
return false
|
||||
}).length === 0}
|
||||
<span class="text-sm">No resource input</span>
|
||||
<span class="text-sm text-gray-600">No resource input</span>
|
||||
{:else}
|
||||
<InputsSpecsEditor
|
||||
id={component.id}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ConnectedAppInput, RowAppInput, StaticAppInput, UserAppInput } from '../inputType'
|
||||
import type { AppComponent } from '../types'
|
||||
import type { AppComponent } from './Component.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
|
||||
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
: 'max-w-6xl'}"
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
<div class={classNames('p-4 mx-auto', width)}>
|
||||
<div class={classNames('pb-4 mx-auto', width)}>
|
||||
<GridEditor {policy} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -186,7 +186,7 @@
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div slot="content" class="h-full overflow-y-auto pb-4">
|
||||
<TabContent class="overflow-auto" value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
<SettingsPanel />
|
||||
@@ -197,7 +197,7 @@
|
||||
<TabContent value="insert">
|
||||
<ComponentList />
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</div>
|
||||
</Tabs>
|
||||
{#if $connectingInput.opened}
|
||||
<div
|
||||
|
||||
@@ -49,10 +49,11 @@
|
||||
StaticAppInput,
|
||||
UserAppInput
|
||||
} from '../inputType'
|
||||
import type { AppComponent, AppEditorContext } from '../types'
|
||||
import type { 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) {
|
||||
@@ -457,7 +458,7 @@
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row grow gap-2 justify-end items-center overflow-visible">
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px] hidden lg:block"
|
||||
|
||||
@@ -14,43 +14,51 @@
|
||||
<Alert type="info" title="Configurations">
|
||||
In order to properly configure the app, you need to fill in the inputs below.
|
||||
</Alert>
|
||||
<Toggle bind:checked={resourceOnly} options={{ right: 'Resource only' }} />
|
||||
{#each $lazyGrid as gridItem (gridItem.data.id)}
|
||||
{#if gridItem.data.type === 'tablecomponent'}
|
||||
<div class="mt-2 flex flex-row-reverse">
|
||||
<Toggle bind:checked={resourceOnly} options={{ right: 'Resource only' }} />
|
||||
</div>
|
||||
<div class="gap-4 flex flex-col pt-4">
|
||||
{#each $lazyGrid as gridItem (gridItem.data.id)}
|
||||
<div>
|
||||
<AppComponentInput bind:component={gridItem.data} {resourceOnly} />
|
||||
<div class="ml-4 mt-4">
|
||||
{#each gridItem.data.actionButtons as actionButton (actionButton.id)}
|
||||
<AppComponentInput bind:component={actionButton.data} {resourceOnly} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if gridItem.data.type === 'tablecomponent'}
|
||||
<div>
|
||||
<AppComponentInput bind:component={gridItem.data} {resourceOnly} />
|
||||
<div class="ml-4 mt-4">
|
||||
{#each gridItem.data.actionButtons as actionButton (actionButton.id)}
|
||||
<AppComponentInput bind:component={actionButton.data} {resourceOnly} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<AppComponentInput bind:component={gridItem.data} {resourceOnly} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<AppComponentInput bind:component={gridItem.data} {resourceOnly} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if $app?.hiddenInlineScripts?.length > 0}
|
||||
<span class="font-bold text-sm">Background script inputs</span>
|
||||
<div class="gap-4 flex flex-col">
|
||||
<div class="font-bold text-lg">Background script inputs</div>
|
||||
<div class="gap-4 flex flex-col pt-4">
|
||||
{#each $app?.hiddenInlineScripts ?? [] as script, index (script.name)}
|
||||
<span class="text-sm">{script.name}</span>
|
||||
<div class="border p-2">
|
||||
<div class="text-sm font-bold">{script.name}</div>
|
||||
|
||||
{#if resourceOnly && Object.keys(script.fields).filter((fieldKey) => {
|
||||
const fields = script.fields
|
||||
const field = fields[fieldKey]
|
||||
return field.fieldType === 'object' && field.format?.startsWith('resource-')
|
||||
}).length === 0}
|
||||
<span class="text-sm">No resource input</span>
|
||||
{:else}
|
||||
<InputsSpecsEditor
|
||||
id={`bg_${index}`}
|
||||
shouldCapitalize={false}
|
||||
bind:inputSpecs={script.fields}
|
||||
userInputEnabled={false}
|
||||
{resourceOnly}
|
||||
/>
|
||||
{/if}
|
||||
{#if resourceOnly && Object.keys(script.fields).filter((fieldKey) => {
|
||||
const fields = script.fields
|
||||
const field = fields[fieldKey]
|
||||
return field.fieldType === 'object' && field.format?.startsWith('resource-')
|
||||
}).length === 0}
|
||||
<span class="text-sm text-gray-600">No resource input</span>
|
||||
{:else}
|
||||
<InputsSpecsEditor
|
||||
id={`bg_${index}`}
|
||||
shouldCapitalize={false}
|
||||
bind:inputSpecs={script.fields}
|
||||
userInputEnabled={false}
|
||||
{resourceOnly}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -12,19 +12,24 @@
|
||||
EditorMode
|
||||
} from '../types'
|
||||
import GridEditor from './GridEditor.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { classNames } from '../../../utils'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import Button from '../../common/button/Button.svelte'
|
||||
import { Unlock } from 'lucide-svelte'
|
||||
|
||||
export let app: App
|
||||
export let appPath: string
|
||||
export let breakpoint: Writable<EditorBreakpoint>
|
||||
export let policy: Policy
|
||||
export let summary: string
|
||||
export let workspace: string
|
||||
export let isEditor: boolean
|
||||
export let context: Record<string, any>
|
||||
export let app: App = {
|
||||
grid: [],
|
||||
fullscreen: false,
|
||||
unusedInlineScripts: [],
|
||||
hiddenInlineScripts: []
|
||||
}
|
||||
export let appPath: string = ''
|
||||
export let breakpoint: Writable<EditorBreakpoint> = writable('lg')
|
||||
export let policy: Policy = {}
|
||||
export let summary: string = ''
|
||||
export let workspace: string = 'workspace'
|
||||
export let isEditor: boolean = false
|
||||
export let context: Record<string, any> = {}
|
||||
export let noBackend: boolean = false
|
||||
export let isLocked = false
|
||||
|
||||
@@ -77,7 +82,7 @@
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="{$$props.class} {lockedClasses} h-full max-h-[calc(100%-41px)] overflow-auto
|
||||
class="{$$props.class} {lockedClasses} h-full
|
||||
w-full {app.fullscreen ? '' : 'max-w-6xl'} mx-auto"
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
|
||||
1332
frontend/src/lib/components/apps/editor/Component.svelte
Normal file
1332
frontend/src/lib/components/apps/editor/Component.svelte
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user