Compare commits

..

1 Commits

Author SHA1 Message Date
Faton Ramadani
5131ee8b89 feat(frontend): add a dev docker-compose file + update readme 2023-04-12 21:46:13 +02:00
124 changed files with 3038 additions and 3663 deletions

View File

@@ -4,8 +4,6 @@ on:
types: [opened,synchronize,reopened,closed]
paths:
- "frontend/**"
merge_group:
jobs:
npm_check:
runs-on: ubuntu-latest

View File

@@ -1,32 +1,6 @@
# Changelog
## [1.88.0](https://github.com/windmill-labs/windmill/compare/v1.87.0...v1.88.0) (2023-04-17)
### Features
* **backend:** install python scripts on save ([cb7e686](https://github.com/windmill-labs/windmill/commit/cb7e686dd95397d5b37edd5aac50b6d1429c4a71))
* **frontend:** Add runs preview popup ([#1405](https://github.com/windmill-labs/windmill/issues/1405)) ([4ab023f](https://github.com/windmill-labs/windmill/commit/4ab023f95085958ab1ad01dc249d308c7ebf423e))
* **frontend:** cancellable inline script editor run ([e828d26](https://github.com/windmill-labs/windmill/commit/e828d2673e62e95e5e1235eeca8107ac7cfb7e45))
* **frontend:** Remove gap when button label is empty ([#1402](https://github.com/windmill-labs/windmill/issues/1402)) ([568f59e](https://github.com/windmill-labs/windmill/commit/568f59eefb104047b8ef063f273fe238075d6407))
* **frontend:** Unify main lists ([#1406](https://github.com/windmill-labs/windmill/issues/1406)) ([48bbbd0](https://github.com/windmill-labs/windmill/commit/48bbbd0e872a12ed1c562a6d14967a2a0f7c4735))
* **frontend:** Update airtable instructions ([#1403](https://github.com/windmill-labs/windmill/issues/1403)) ([7dc7ece](https://github.com/windmill-labs/windmill/commit/7dc7ecef55b465fc096f71fc9de5c8b543136ff7))
* inputs library on run page ([92a2934](https://github.com/windmill-labs/windmill/commit/92a293488e8e58350229931ab69f7924d58474be))
### Bug Fixes
* **backend:** deno uses --no-check ([a5499c2](https://github.com/windmill-labs/windmill/commit/a5499c26f3ebd8b07541a7e0cbf33a7008a8f476))
* **backend:** do not fail on schedule not existing anymore ([a5f6d73](https://github.com/windmill-labs/windmill/commit/a5f6d73f7d53d7af9d285a85460509763263c508))
* **frontend:** Fix app file uploads ([#1408](https://github.com/windmill-labs/windmill/issues/1408)) ([ac489ac](https://github.com/windmill-labs/windmill/commit/ac489ac2da0fbf01f5e2877612c14cfaf1ef79c2))
* **frontend:** fix buttons width ([#1407](https://github.com/windmill-labs/windmill/issues/1407)) ([75a0482](https://github.com/windmill-labs/windmill/commit/75a0482ef046dd7e30f6d6039dbc66880182dc5e))
* **frontend:** fix enum sync ([#1410](https://github.com/windmill-labs/windmill/issues/1410)) ([98060ce](https://github.com/windmill-labs/windmill/commit/98060ce55d5efa59a8989cf9357935976d57650b))
* **frontend:** Handle scheduled runs in preview ([#1413](https://github.com/windmill-labs/windmill/issues/1413)) ([accdc1a](https://github.com/windmill-labs/windmill/commit/accdc1ac59ce9611f66567222a73995d3c0a3f9d))
* **frontend:** Keep selected tab during renaming ([#1409](https://github.com/windmill-labs/windmill/issues/1409)) ([82cd048](https://github.com/windmill-labs/windmill/commit/82cd048ef4d08f31660f6f31a96940676a28996c))
* **frontend:** Queued-running jobs preview ([#1414](https://github.com/windmill-labs/windmill/issues/1414)) ([b2a40a0](https://github.com/windmill-labs/windmill/commit/b2a40a05805344c1c34f2ba917b4cdd52dfffc3f))
* **frontend:** Remove output when deleting a component ([#1397](https://github.com/windmill-labs/windmill/issues/1397)) ([6aa1008](https://github.com/windmill-labs/windmill/commit/6aa100893352870d5a99fdd56d7f1425a221a273))
## [1.87.0](https://github.com/windmill-labs/windmill/compare/v1.86.0...v2.0.0) (2023-04-11)

View File

@@ -334,8 +334,7 @@ it being synced automatically everyday.
| 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 |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails |
| GLOBAL_CACHE_INTERVAL | 10\*60 | (Enterprise Edition only) Interval in seconds in between bucket sync of the cache. This interval \* 2 is the time at which you're guaranteed all the worker's caches are synced together. | Worker |
| INSTANCE_EVENTS_WEBHOOK | None | Webhook to notify of events such as new user added, signup/invite. Can hook back to windmill to send emails | Server |
## Run a local dev setup

View File

@@ -1,4 +1,3 @@
{
"python.analysis.typeCheckingMode": "basic",
"rust-analyzer.linkedProjects": ["./windmill-common/Cargo.toml"]
"python.analysis.typeCheckingMode": "basic"
}

170
backend/Cargo.lock generated
View File

@@ -72,51 +72,42 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.3.0"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371"
checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"concolor-override",
"concolor-query",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.0"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2"
[[package]]
name = "anstyle-parse"
version = "0.2.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd"
checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
"windows-sys 0.45.0",
]
[[package]]
@@ -272,7 +263,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -349,7 +340,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -390,9 +381,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.15"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b32c5ea3aabaf4deb5f5ced2d688ec0844c881c9e6c696a8b769a05fc691e62"
checksum = "6e5abc76e6ef57bf438c067d2898481475994102eeb5d662a0b2162d0c2fdcf1"
dependencies = [
"async-trait",
"axum-core",
@@ -694,9 +685,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.2.2"
version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b802d85aaf3a1cdb02b224ba472ebdea62014fccfcb269b95a4d76443b5ee5a"
checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3"
dependencies = [
"clap_builder",
"clap_derive",
@@ -705,9 +696,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.2.2"
version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a1a858f532119338887a4b8e1af9c60de8249cd7bafd68036a489e261e37b6"
checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f"
dependencies = [
"anstream",
"anstyle",
@@ -725,7 +716,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -744,12 +735,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "combine"
version = "4.6.6"
@@ -764,6 +749,21 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "concolor-override"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f"
[[package]]
name = "concolor-query"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf"
dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "concurrent-queue"
version = "2.2.0"
@@ -955,7 +955,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -972,7 +972,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -1024,9 +1024,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]]
name = "deno_core"
version = "0.180.0"
version = "0.179.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ee6088fd6a1d653eed4068655ed3bcf5166de2a9f83c00f1bd747b593210bd"
checksum = "8c9307ca2299cb7b0bdaa345cbdc82a252a8e4e5a4463e28f44c715d55e460fb"
dependencies = [
"anyhow",
"bytes",
@@ -1049,9 +1049,9 @@ dependencies = [
[[package]]
name = "deno_ops"
version = "0.58.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04c9b5edeb8c3892943a38e6fcc28eac6655a0ef943c95b4c23b4305f29646e3"
checksum = "04610f07342fbb33a2b7ea7aa16a95ab71adb13a0ce858a8d1a1414660a83e3e"
dependencies = [
"once_cell",
"pmutil",
@@ -1426,7 +1426,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -1559,9 +1559,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.18"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
dependencies = [
"bytes",
"fnv",
@@ -1748,9 +1748,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.26"
version = "0.14.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
dependencies = [
"bytes",
"futures-channel",
@@ -2436,7 +2436,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -2782,7 +2782,7 @@ dependencies = [
[[package]]
name = "progenitor"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#c8f5c0e0384c2e16b1079cb94db4fcd870fc8e66"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"anyhow",
"built",
@@ -2800,7 +2800,7 @@ dependencies = [
[[package]]
name = "progenitor-client"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#c8f5c0e0384c2e16b1079cb94db4fcd870fc8e66"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"bytes",
"futures-core",
@@ -2814,7 +2814,7 @@ dependencies = [
[[package]]
name = "progenitor-impl"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#c8f5c0e0384c2e16b1079cb94db4fcd870fc8e66"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"getopts",
"heck",
@@ -2827,7 +2827,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"syn 2.0.15",
"syn 2.0.14",
"thiserror",
"typify",
"unicode-ident",
@@ -2836,7 +2836,7 @@ dependencies = [
[[package]]
name = "progenitor-macro"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#c8f5c0e0384c2e16b1079cb94db4fcd870fc8e66"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"openapiv3",
"proc-macro2",
@@ -2847,7 +2847,7 @@ dependencies = [
"serde_json",
"serde_tokenstream",
"serde_yaml",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -3262,7 +3262,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#5f17d281a6f23848e211fdc58979327d1e6a5a30"
source = "git+https://github.com/RustPython/RustPython#c9546c2419f2e4db7e2987f30f0508dc29b30035"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -3271,7 +3271,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#5f17d281a6f23848e211fdc58979327d1e6a5a30"
source = "git+https://github.com/RustPython/RustPython#c9546c2419f2e4db7e2987f30f0508dc29b30035"
dependencies = [
"bitflags",
"bstr",
@@ -3284,7 +3284,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#5f17d281a6f23848e211fdc58979327d1e6a5a30"
source = "git+https://github.com/RustPython/RustPython#c9546c2419f2e4db7e2987f30f0508dc29b30035"
dependencies = [
"ahash 0.7.6",
"anyhow",
@@ -3491,7 +3491,7 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -3507,9 +3507,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.96"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
dependencies = [
"indexmap",
"itoa",
@@ -3557,7 +3557,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -3574,9 +3574,9 @@ dependencies = [
[[package]]
name = "serde_v8"
version = "0.91.0"
version = "0.90.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac73d1b5fabb8c9e1f44533520d1d14434f462b2c29ba21c30cb544a1c8181d"
checksum = "916ca7852a4c5f0ba59ce4a46301bf7c7ad573c2c89a0fe67e90fe30dcbd6f7d"
dependencies = [
"bytes",
"derive_more",
@@ -4129,9 +4129,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.15"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5"
dependencies = [
"proc-macro2",
"quote",
@@ -4194,7 +4194,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -4308,7 +4308,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.14",
]
[[package]]
@@ -4603,7 +4603,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typify"
version = "0.0.12-dev"
source = "git+https://github.com/oxidecomputer/typify#7393e427c34f10fef45f02df5c906a1e925929f8"
source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4"
dependencies = [
"typify-impl",
"typify-macro",
@@ -4612,7 +4612,7 @@ dependencies = [
[[package]]
name = "typify-impl"
version = "0.0.12-dev"
source = "git+https://github.com/oxidecomputer/typify#7393e427c34f10fef45f02df5c906a1e925929f8"
source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4"
dependencies = [
"heck",
"log",
@@ -4621,7 +4621,7 @@ dependencies = [
"regress",
"schemars",
"serde_json",
"syn 2.0.15",
"syn 2.0.14",
"thiserror",
"unicode-ident",
]
@@ -4629,7 +4629,7 @@ dependencies = [
[[package]]
name = "typify-macro"
version = "0.0.12-dev"
source = "git+https://github.com/oxidecomputer/typify#7393e427c34f10fef45f02df5c906a1e925929f8"
source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4"
dependencies = [
"proc-macro2",
"quote",
@@ -4637,7 +4637,7 @@ dependencies = [
"serde",
"serde_json",
"serde_tokenstream",
"syn 2.0.15",
"syn 2.0.14",
"typify-impl",
]
@@ -5096,7 +5096,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"axum",
@@ -5105,9 +5105,6 @@ dependencies = [
"dotenv",
"futures",
"git-version",
"lazy_static",
"once_cell",
"prometheus",
"rand 0.8.5",
"reqwest",
"rsa",
@@ -5120,7 +5117,6 @@ dependencies = [
"tokio-metrics",
"tracing",
"url",
"uuid 1.3.1",
"windmill-api",
"windmill-api-client",
"windmill-common",
@@ -5130,7 +5126,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"argon2",
@@ -5188,7 +5184,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"base64 0.21.0",
"chrono",
@@ -5203,7 +5199,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"chrono",
"serde",
@@ -5216,7 +5212,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"axum",
@@ -5241,7 +5237,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"serde",
"serde_json",
@@ -5249,7 +5245,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"itertools",
@@ -5264,7 +5260,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"gosyn",
@@ -5277,7 +5273,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"itertools",
@@ -5292,7 +5288,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"deno_core",
@@ -5306,7 +5302,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"chrono",
@@ -5333,7 +5329,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.88.0"
version = "1.87.0"
dependencies = [
"anyhow",
"async-recursion",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.88.0"
version = "1.87.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.88.0"
version = "1.87.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -50,10 +50,6 @@ base64.workspace = true
sha2.workspace = true
rsmq_async.workspace = true
url.workspace = true
lazy_static.workspace = true
once_cell.workspace = true
prometheus.workspace = true
uuid.workspace = true
[dev-dependencies]

View File

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

View File

@@ -1,3 +0,0 @@
-- Add up migration script here
GRANT ALL ON input TO windmill_user;
GRANT ALL ON input TO windmill_admin;

View File

@@ -14,20 +14,6 @@
},
"query": "UPDATE usr SET disabled = $1 WHERE username = $2 AND workspace_id = $3"
},
"01576057b71e32e20a1702b2c89c380eee422060a65b475d7cda8b199d70842e": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Bool",
"Text",
"Text"
]
}
},
"query": "UPDATE flow SET archived = $1 WHERE path = $2 AND workspace_id = $3"
},
"019258392434b3c8dfabfe53d61ad766626fe4ad67f101c1a58c9c9524531621": {
"describe": {
"columns": [
@@ -2275,6 +2261,19 @@
},
"query": "SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND workspace_id = $2"
},
"63f330e98051ae9d0cf5617a553f769cc98b05fdc8643839b504023b26f38aab": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2"
},
"64920b845c0ce81fb99497c03b249bb6cb06581079b5fc5bea5ddd8e7a895b79": {
"describe": {
"columns": [
@@ -5266,39 +5265,6 @@
},
"query": "SELECT count(path) FROM app WHERE path LIKE 'f/' || $1 || '%' AND workspace_id = $2"
},
"d243064655e45a3898e39ed151416e4e63ec19292cedddfb408d026cf5218f09": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "write",
"ordinal": 1,
"type_info": "Bool"
},
{
"name": "owner",
"ordinal": 2,
"type_info": "Bool"
}
],
"nullable": [
false,
null,
null
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
}
},
"query": "SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write, $1 && owners::text[] as owner FROM folder\n WHERE extra_perms ?| $1 AND workspace_id = $2"
},
"d47bff7d6b54cd6da8bb330f7321c37af5dcbd76f9acad73b5ba1b8a4afb5091": {
"describe": {
"columns": [
@@ -5921,6 +5887,29 @@
},
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *"
},
"ebc06efe51532f7f60e98f62d09a3453fc1d97d93e96ec849eac96dca05236f0": {
"describe": {
"columns": [
{
"name": "?column?",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text",
"Text",
"TextArray",
"Text"
]
}
},
"query": "SELECT EXISTS(SELECT 1 FROM folder WHERE CONCAT('u/', $1::text) = ANY(owners) AND name = $2 AND workspace_id = $4) OR exists(\n SELECT 1 FROM folder, unnest(folder.owners) as o\n WHERE o = ANY($3::text[]) AND folder.name = $2 AND folder.workspace_id = $4)"
},
"ec85a425f88044c6ed4f8fcea223c28eb9fb8c16c89a52d4c4552bd149badafa": {
"describe": {
"columns": [
@@ -6208,6 +6197,33 @@
},
"query": "DELETE FROM usr_to_group WHERE usr = $1 AND group_ = $2 AND workspace_id = $3"
},
"f418ad25df9bd0a85c7210c4ea0f44c3b6e8c0810b49e325fd0698259f0bb1a3": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "write",
"ordinal": 1,
"type_info": "Bool"
}
],
"nullable": [
false,
null
],
"parameters": {
"Left": [
"TextArray",
"Text"
]
}
},
"query": "SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write FROM folder\n WHERE extra_perms ?| $1 AND workspace_id = $2"
},
"f4849960aff7387cb6b130d2bc62dbfce45209fbbccf3b4b9b13019e0cd55ddb": {
"describe": {
"columns": [

View File

@@ -9,7 +9,6 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use git_version::git_version;
use monitor::handle_zombie_jobs_periodically;
use sqlx::{Pool, Postgres};
use windmill_common::{utils::rd_string, METRICS_ADDR};
@@ -19,7 +18,6 @@ const DEFAULT_PORT: u16 = 8000;
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
mod ee;
mod monitor;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@@ -232,7 +230,7 @@ pub fn monitor_db<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 'static>
let rx2 = rx.resubscribe();
let base_internal_url = base_internal_url.to_string();
tokio::spawn(async move {
handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq).await
windmill_worker::handle_zombie_jobs_periodically(&db1, rx, &base_internal_url, rsmq).await
});
tokio::spawn(async move { windmill_api::delete_expired_items_perdiodically(&db2, rx2).await });
}

View File

@@ -1,148 +0,0 @@
use std::time::Duration;
use once_cell::sync::OnceCell;
use sqlx::{Pool, Postgres};
use tokio::sync::mpsc;
use uuid::Uuid;
use windmill_common::{error, jobs::{JobKind, QueuedJob}, METRICS_ENABLED};
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SESSION_TOKEN_EXPIRY,
};
lazy_static::lazy_static! {
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
.ok()
.and_then(|x| x.parse::<String>().ok())
.unwrap_or_else(|| "30".to_string());
pub static ref RESTART_ZOMBIE_JOBS: bool = std::env::var("RESTART_ZOMBIE_JOBS")
.ok()
.and_then(|x| x.parse::<bool>().ok())
.unwrap_or(true);
static ref QUEUE_ZOMBIE_RESTART_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_restart_count",
"Total number of jobs restarted due to ping timeout."
)
.unwrap();
static ref QUEUE_ZOMBIE_DELETE_COUNT: prometheus::IntCounter = prometheus::register_int_counter!(
"queue_zombie_delete_count",
"Total number of jobs deleted due to their ping timing out in an unrecoverable state."
)
.unwrap();
}
pub async fn handle_zombie_jobs_periodically<
R: rsmq_async::RsmqConnection + Send + Sync + Clone,
>(
db: &Pool<Postgres>,
mut rx: tokio::sync::broadcast::Receiver<()>,
base_internal_url: &str,
rsmq: Option<R>,
) {
loop {
handle_zombie_jobs(db, base_internal_url, rsmq.clone()).await;
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(30)) => (),
_ = rx.recv() => {
println!("received killpill for monitor job");
break;
}
}
}
}
async fn handle_zombie_jobs<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
db: &Pool<Postgres>,
base_internal_url: &str,
rsmq: Option<R>,
) {
if *RESTART_ZOMBIE_JOBS {
let restarted = sqlx::query!(
"UPDATE queue SET running = false, started_at = null, logs = logs || '\nRestarted job after not receiving job''s ping for too long the ' || now() || '\n\n' WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2 AND job_kind != $3 AND same_worker = false RETURNING id, workspace_id, last_ping",
*ZOMBIE_JOB_TIMEOUT,
JobKind::Flow: JobKind,
JobKind::FlowPreview: JobKind,
)
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
if *METRICS_ENABLED {
QUEUE_ZOMBIE_RESTART_COUNT.inc_by(restarted.len() as _);
}
for r in restarted {
tracing::info!(
"restarted zombie job {} {} {}",
r.id,
r.workspace_id,
r.last_ping
);
}
}
let mut timeout_query = "SELECT * FROM queue WHERE last_ping < now() - ($1 || ' seconds')::interval AND running = true AND job_kind != $2".to_string();
if *RESTART_ZOMBIE_JOBS {
timeout_query.push_str(" AND same_worker = true");
};
let timeouts = sqlx::query_as::<_, QueuedJob>(&timeout_query)
.bind(ZOMBIE_JOB_TIMEOUT.as_str())
.bind(JobKind::Flow)
.fetch_all(db)
.await
.ok()
.unwrap_or_else(|| vec![]);
if *METRICS_ENABLED {
QUEUE_ZOMBIE_DELETE_COUNT.inc_by(timeouts.len() as _);
}
for job in timeouts {
tracing::info!("timedout zombie job {} {}", job.id, job.workspace_id,);
// since the job is unrecoverable, the same worker queue should never be sent anything
let (same_worker_tx_never_used, _same_worker_rx_never_used) = mpsc::channel::<Uuid>(1);
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
"ephemeral-zombie-jobs",
*SESSION_TOKEN_EXPIRY,
&job.email,
)
.await
.expect("could not create job token");
let client = AuthedClient {
base_internal_url: base_internal_url.to_string(),
token,
workspace: job.workspace_id.to_string(),
client: OnceCell::new(),
};
let last_ping = job.last_ping.clone();
let _ = handle_job_error(
db,
&client,
job,
error::Error::ExecutionErr(format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {})",
last_ping
.map(|x| x.to_string())
.unwrap_or_else(|| "no ping".to_string()),
*ZOMBIE_JOB_TIMEOUT
)),
None,
true,
same_worker_tx_never_used,
"",
base_internal_url,
rsmq.clone(),
)
.await;
}
}

View File

@@ -6,10 +6,9 @@ use windmill_api::jobs::{CompletedJob, Job};
use windmill_common::{
flow_status::{FlowStatus, FlowStatusModule},
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
jobs::{JobPayload, RawCode},
scripts::ScriptLang,
};
use windmill_queue::get_queued_job;
use windmill_queue::{get_queued_job, JobPayload, RawCode};
async fn initialize_tracing() {
use std::sync::Once;
@@ -129,7 +128,8 @@ mod suspend_resume {
use futures::{Stream, StreamExt};
use serde_json::json;
use sqlx::{query_scalar, types::Uuid};
use windmill_common::{flows::FlowValue, jobs::JobPayload};
use windmill_common::flows::FlowValue;
use windmill_queue::JobPayload;
use super::*;
@@ -393,6 +393,7 @@ mod retry {
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use windmill_common::flows::FlowValue;
use windmill_queue::JobPayload;
use super::*;
@@ -2452,7 +2453,6 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
.modules
.into_iter()
.for_each(|m| {
tracing::error!("Module: {:?}", m.value);
assert!(matches!(
m.value,
windmill_api_client::types::FlowModuleValue::Rawscript {

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.88.0
version: 1.87.0
title: Windmill API
contact:
@@ -2757,16 +2757,6 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
requestBody:
description: archiveFlow
required: true
content:
application/json:
schema:
type: object
properties:
archived:
type: boolean
responses:
"200":
description: flow archived
@@ -5290,10 +5280,6 @@ components:
type: array
items:
type: string
folders_owners:
type: array
items:
type: string
usage:
$ref: "#/components/schemas/Usage"
required:
@@ -5305,7 +5291,6 @@ components:
- operator
- disabled
- folders
- folders_owners
Usage:
type: object

View File

@@ -32,13 +32,12 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
apps::ListAppQuery,
error::{to_anyhow, Error, JsonResult, Result},
jobs::{JobPayload, RawCode},
users::username_to_permissioned_as,
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, QueueTransaction};
use windmill_queue::{push, JobPayload, QueueTransaction, RawCode};
pub fn workspaced_service() -> Router {
Router::new()
@@ -442,6 +441,7 @@ 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>,
) -> Result<String> {
@@ -458,7 +458,10 @@ async fn update_app(
if let Some(npath) = &ns.path {
if npath != path {
require_owner_of_path(&authed, path)?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db)
.await?;
}
}
sqlb.set_str("path", npath);
}

View File

@@ -19,7 +19,6 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde::Deserialize;
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{Postgres, Transaction};
@@ -27,13 +26,12 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error, JsonResult, Result},
flows::{Flow, ListFlowQuery, ListableFlow, NewFlow},
jobs::JobPayload,
schedule::Schedule,
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
};
use windmill_queue::{push, schedule::push_scheduled_job, QueueTransaction};
use windmill_queue::{push, schedule::push_scheduled_job, JobPayload, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -87,8 +85,9 @@ async fn list_flows(
.limit(per_page)
.clone();
sqlb.and_where_eq("archived", lq.show_archived.unwrap_or(false));
if !lq.show_archived.unwrap_or(false) {
sqlb.and_where_eq("archived", false);
}
if let Some(ps) = &lq.path_start {
sqlb.and_where_like_left("path", "?".bind(ps));
}
@@ -326,7 +325,7 @@ async fn update_flow(
check_schedule_conflict(tx.transaction_mut(), &w_id, &nf.path).await?;
if !authed.is_admin {
require_owner_of_path(&authed, flow_path)?;
require_owner_of_path(&w_id, &authed.username, &authed.groups, &flow_path, &db).await?;
}
let mut schedulables: Vec<Schedule> = sqlx::query_as!(
@@ -463,24 +462,17 @@ async fn exists_flow_by_path(
Ok(Json(exists))
}
#[derive(Deserialize)]
struct Archived {
archived: Option<bool>,
}
async fn archive_flow_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(archived): Json<Archived>,
) -> Result<String> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
sqlx::query!(
"UPDATE flow SET archived = $1 WHERE path = $2 AND workspace_id = $3",
archived.archived.unwrap_or(true),
"UPDATE flow SET archived = true WHERE path = $1 AND workspace_id = $2",
path,
&w_id
)

View File

@@ -22,13 +22,13 @@ use lazy_static::lazy_static;
use regex::Regex;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, JsonResult, Result},
error::{self, to_anyhow, Error, JsonResult, Result},
users::username_to_permissioned_as,
utils::{not_found_if_none, paginate, Pagination},
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Postgres, Transaction};
use sqlx::{query_scalar, FromRow, Postgres, Transaction};
pub fn workspaced_service() -> Router {
Router::new()
@@ -41,7 +41,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/:name", delete(delete_folder))
.route("/addowner/:name", post(add_owner))
.route("/removeowner/:name", post(remove_owner))
.route("/is_owner/*path", get(is_owner_api))
.route("/is_owner/*path", get(is_owner))
}
#[derive(FromRow, Serialize, Deserialize, Clone)]
@@ -219,29 +219,47 @@ async fn create_folder(
Ok(format!("Created folder {}", ng.name))
}
pub async fn is_owner_api(
authed: Authed,
Path((_w_id, name)): Path<(String, String)>,
pub async fn is_owner(
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, name)): Path<(String, String)>,
) -> JsonResult<bool> {
Ok(Json(is_owner(&authed, &name)))
}
pub fn is_owner(Authed { is_admin, folders, .. }: &Authed, name: &str) -> bool {
if *is_admin {
true
if is_admin {
Ok(Json(true))
} else {
folders.into_iter().any(|x| x.0 == name && x.2)
Ok(Json(
require_is_owner(&name, &username, &groups, &w_id, &db)
.await
.is_ok(),
))
}
}
pub fn require_is_owner(authed: &Authed, name: &str) -> Result<()> {
if is_owner(authed, name) {
Ok(())
} else {
Err(windmill_common::error::Error::NotAuthorized(format!(
"You are not owner of the folder {}",
name
pub async fn require_is_owner(
folder_name: &str,
username: &str,
groups: &Vec<String>,
w_id: &str,
db: &DB,
) -> Result<()> {
let is_owner = query_scalar!(
"SELECT EXISTS(SELECT 1 FROM folder WHERE CONCAT('u/', $1::text) = ANY(owners) AND name = $2 AND workspace_id = $4) OR exists(
SELECT 1 FROM folder, unnest(folder.owners) as o
WHERE o = ANY($3::text[]) AND folder.name = $2 AND folder.workspace_id = $4)",
username,
folder_name,
groups,
w_id,
).fetch_one(db)
.await?
.unwrap_or(false);
if !is_owner {
Err(Error::BadRequest(format!(
"{} is not an owner of {} and hence is not authorized to perform this operation",
username, folder_name
)))
} else {
Ok(())
}
}
@@ -473,6 +491,7 @@ async fn delete_folder(
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)>,
@@ -481,7 +500,9 @@ async fn add_owner(
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
require_is_owner(&authed, &name)?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
sqlx::query!(
"UPDATE folder SET owners = array_append(owners, $1) WHERE name = $2 AND workspace_id = $3 AND NOT $1 = ANY(owners) RETURNING name",
@@ -517,14 +538,14 @@ pub async fn get_folders_for_user(
username: &str,
groups: &[String],
db: &DB,
) -> Result<Vec<(String, bool, bool)>> {
) -> Result<Vec<(String, bool)>> {
let mut perms = groups
.into_iter()
.map(|x| format!("g/{}", x))
.collect::<Vec<_>>();
perms.insert(0, format!("u/{}", username));
let folders = sqlx::query!(
"SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write, $1 && owners::text[] as owner FROM folder
"SELECT name, (EXISTS (SELECT 1 FROM (SELECT key, value FROM jsonb_each_text(extra_perms) WHERE key = ANY($1)) t WHERE value::boolean IS true)) as write FROM folder
WHERE extra_perms ?| $1 AND workspace_id = $2",
&perms[..],
w_id,
@@ -532,7 +553,7 @@ pub async fn get_folders_for_user(
.fetch_all(db)
.await?
.into_iter()
.map(|x| (x.name, x.write.unwrap_or(false), x.owner.unwrap_or(false)))
.map(|x| (x.name, x.write.unwrap_or(false)))
.collect();
Ok(folders)
@@ -540,6 +561,7 @@ pub async fn get_folders_for_user(
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)>,
@@ -548,7 +570,9 @@ async fn remove_owner(
let mut tx = user_db.begin(&authed).await?;
not_found_if_none(get_folderopt(&mut tx, &w_id, &name).await?, "Folder", &name)?;
require_is_owner(&authed, &name)?;
if !authed.is_admin {
require_is_owner(&name, &authed.username, &authed.groups, &w_id, &db).await?;
}
sqlx::query!(
"UPDATE folder SET owners = array_remove(owners, $1) WHERE name = $2 AND workspace_id = $3 RETURNING name",

View File

@@ -43,29 +43,25 @@ async fn add_granular_acl(
Json(GranularAcl { owner, write }): Json<GranularAcl>,
) -> Result<String> {
let path = path.to_path();
let (kind, path) = path
.split_once('/')
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
let mut tx = user_db.begin(&authed).await?;
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else if kind == "group_" {
} else {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
}
let identifier = if kind == "group_" || kind == "folder" {
"name"
} else {
"path"
};
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&authed, path)?;
} else if kind == "group_" {
crate::groups::require_is_owner(path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else {
require_owner_of_path(&authed, path)?;
}
}
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, '{{\"{owner}\"}}', to_jsonb($1), \
true) WHERE {identifier} = $2 AND workspace_id = $3 RETURNING extra_perms"
@@ -90,22 +86,12 @@ async fn remove_granular_acl(
Json(GranularAcl { owner, write: _ }): Json<GranularAcl>,
) -> Result<String> {
let path = path.to_path();
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let (kind, path) = path
.split_once('/')
.ok_or_else(|| Error::BadRequest("Invalid path or kind".to_string()))?;
if !authed.is_admin {
if kind == "folder" {
crate::folders::require_is_owner(&authed, path)?;
} else if kind == "group_" {
crate::groups::require_is_owner(path, &authed.username, &authed.groups, &w_id, &db)
.await?;
} else {
require_owner_of_path(&authed, path)?;
}
}
let mut tx = user_db.begin(&authed).await?;
let identifier = if kind == "group_" || kind == "folder" {
@@ -113,11 +99,6 @@ async fn remove_granular_acl(
} else {
"path"
};
if identifier == "path" {
require_owner_of_path(&authed, path)?;
}
let obj_o = sqlx::query_scalar::<_, serde_json::Value>(&format!(
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
workspace_id = $3 RETURNING extra_perms"

View File

@@ -22,10 +22,11 @@ use std::{
};
use windmill_common::{
error::JsonResult,
jobs::JobKind,
scripts::to_i64,
utils::{paginate, Pagination},
};
use windmill_queue::JobKind;
pub fn workspaced_service() -> Router {
Router::new()
.route("/history", get(get_input_history))
@@ -112,9 +113,9 @@ async fn get_input_history(
let mut tx = user_db.begin(&authed).await?;
let sql = &format!(
"select * from (select distinct on (args) * from completed_job \
"select distinct on (args) * from completed_job \
where {} = $1 and job_kind = $2 and workspace_id = $3 \
order by args, started_at desc) t ORDER BY started_at desc limit $4 offset $5",
order by args, started_at desc limit $4 offset $5",
r.runnable_type.column_name()
);

View File

@@ -31,13 +31,14 @@ use windmill_common::{
error::{self, to_anyhow, Error},
flow_status::{Approval, FlowStatus, FlowStatusModule},
flows::FlowValue,
jobs::{JobKind, JobPayload, QueuedJob, RawCode},
oauth2::HmacSha256,
scripts::{ScriptHash, ScriptLang},
users::username_to_permissioned_as,
utils::{not_found_if_none, now_from_db, paginate, require_admin, Pagination, StripPath},
};
use windmill_queue::{get_queued_job, push, QueueTransaction};
use windmill_queue::{
get_queued_job, push, JobKind, JobPayload, QueueTransaction, QueuedJob, RawCode,
};
pub fn workspaced_service() -> Router {
Router::new()
@@ -626,7 +627,7 @@ async fn list_jobs(
pub async fn resume_suspended_flow_as_owner(
authed: Authed,
Extension(db): Extension<DB>,
Path((_w_id, flow_id)): Path<(String, Uuid)>,
Path((w_id, flow_id)): Path<(String, Uuid)>,
QueryOrBody(value): QueryOrBody<serde_json::Value>,
) -> error::Result<StatusCode> {
let value = value.unwrap_or(serde_json::Value::Null);
@@ -634,11 +635,16 @@ pub async fn resume_suspended_flow_as_owner(
let (flow, job_id) = get_suspended_flow_info(flow_id, &mut tx).await?;
require_owner_of_path(
&authed,
&flow.script_path.clone().unwrap_or_else(|| String::new()),
)?;
if !authed.is_admin {
require_owner_of_path(
&w_id,
&authed.username,
&authed.groups,
&flow.script_path.clone().unwrap_or_else(|| String::new()),
&db,
)
.await?;
}
insert_resume_job(0, job_id, &flow, value, Some(authed.username), &mut tx).await?;
resume_immediately_if_relevant(flow, job_id, &mut tx).await?;

View File

@@ -29,7 +29,6 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sqlx::{Postgres, Transaction};
use tower_cookies::{Cookie, Cookies};
use windmill_audit::{audit_log, ActionKind};
use windmill_common::jobs::JobPayload;
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::{not_found_if_none, now_from_db};
@@ -45,7 +44,7 @@ use crate::{BASE_URL, HTTP_CLIENT, IS_SECURE, OAUTH_CLIENTS, SLACK_SIGNING_SECRE
use windmill_common::error::{self, to_anyhow, Error};
use windmill_common::oauth2::*;
use windmill_queue::QueueTransaction;
use windmill_queue::{JobPayload, QueueTransaction};
use std::{fs, str};

View File

@@ -386,8 +386,9 @@ async fn update_resource(
if npath != path {
check_path_conflict(&mut tx, &w_id, &npath).await?;
require_owner_of_path(&authed, path)?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
sqlx::query!(
"UPDATE variable SET path = $1 WHERE path = $2 AND workspace_id = $3",
npath,

View File

@@ -23,9 +23,9 @@ use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
schedule::Schedule,
utils::{not_found_if_none, paginate, Pagination, StripPath}, jobs::JobKind,
utils::{not_found_if_none, paginate, Pagination, StripPath},
};
use windmill_queue::{self, schedule::push_scheduled_job, QueueTransaction};
use windmill_queue::{self, schedule::push_scheduled_job, JobKind, QueueTransaction};
pub fn workspaced_service() -> Router {
Router::new()

View File

@@ -32,7 +32,6 @@ use std::{
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
jobs::JobPayload,
schedule::Schedule,
scripts::{
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash,
@@ -273,7 +272,10 @@ async fn create_script(
let ps = get_script_by_hash_internal(tx.transaction_mut(), &w_id, p_hash).await?;
if ps.path != ns.path {
require_owner_of_path(&authed, &ps.path)?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &ps.path, &db)
.await?;
}
}
let ph = {
@@ -426,7 +428,7 @@ async fn create_script(
let (_, new_tx) = windmill_queue::push(
tx,
&w_id,
JobPayload::Dependencies { hash, dependencies, language: ns.language },
windmill_queue::JobPayload::Dependencies { hash, dependencies, language: ns.language },
serde_json::Map::new(),
&authed.username,
&authed.email,
@@ -634,8 +636,6 @@ async fn archive_script_by_path(
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
require_owner_of_path(&authed, path)?;
let hash: i64 = sqlx::query_scalar!(
"UPDATE script SET archived = true WHERE path = $1 AND workspace_id = $2 RETURNING hash",
path,

View File

@@ -359,8 +359,7 @@ pub struct Authed {
pub username: String,
pub is_admin: bool,
pub groups: Vec<String>,
// (folder name, can write, is owner)
pub folders: Vec<(String, bool, bool)>,
pub folders: Vec<(String, bool)>,
}
pub async fn maybe_refresh_folders(path: &str, w_id: &str, authed: Authed, db: &DB) -> Authed {
@@ -370,7 +369,7 @@ pub async fn maybe_refresh_folders(path: &str, w_id: &str, authed: Authed, db: &
let splitted = path.split('/').collect::<Vec<_>>();
if splitted.len() >= 2
&& splitted[0] == "f"
&& !authed.folders.iter().any(|(f, _, _)| f == splitted[1])
&& !authed.folders.iter().any(|(f, _)| f == splitted[1])
{
let name = &authed.username;
let groups = get_groups_for_user(w_id, name, db)
@@ -510,7 +509,6 @@ pub struct UserInfo {
pub disabled: bool,
pub role: Option<String>,
pub folders: Vec<String>,
pub folders_owners: Vec<String>,
}
#[derive(FromRow, Serialize)]
@@ -810,14 +808,9 @@ async fn whoami(
disabled: false,
role: Some("superadmin".to_string()),
folders: folders
.clone()
.into_iter()
.filter_map(|x| if x.1 { Some(x.0) } else { None })
.collect(),
folders_owners: folders
.into_iter()
.filter_map(|x| if x.2 { Some(x.0) } else { None })
.collect(),
}))
}
}
@@ -903,14 +896,9 @@ async fn get_user(w_id: &str, username: &str, db: &DB) -> Result<Option<UserInfo
disabled: usr.disabled,
role: usr.role,
folders: folders
.clone()
.into_iter()
.filter_map(|x| if x.1 { Some(x.0) } else { None })
.collect(),
folders_owners: folders
.into_iter()
.filter_map(|x| if x.2 { Some(x.0) } else { None })
.collect(),
}))
}
@@ -925,42 +913,47 @@ pub async fn get_groups_for_user(w_id: &str, username: &str, db: &DB) -> Result<
Ok(groups)
}
pub async fn is_owner_of_path(
authed: Authed,
Path((_w_id, path)): Path<(String, StripPath)>,
Authed { username, is_admin, groups, .. }: Authed,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<bool> {
let path = path.to_path();
if authed.is_admin {
if is_admin {
Ok(Json(true))
} else {
Ok(Json(require_owner_of_path(&authed, path).is_ok()))
Ok(Json(
require_owner_of_path(&w_id, &username, &groups, path, &db)
.await
.is_ok(),
))
}
}
pub fn require_owner_of_path(authed: &Authed, path: &str) -> Result<()> {
pub async fn require_owner_of_path(
w_id: &str,
username: &str,
groups: &Vec<String>,
path: &str,
db: &DB,
) -> Result<()> {
if !path.is_empty() {
let splitted = path.split("/").collect::<Vec<&str>>();
if splitted[0] == "u" {
if splitted[1] == authed.username {
Ok(())
if splitted[1] == username {
return Ok(());
} else {
Err(Error::BadRequest(format!(
return Err(Error::BadRequest(format!(
"only the owner {} is authorized to perform this operation",
splitted[1]
)))
)));
}
} else if splitted[0] == "g" {
return crate::groups::require_is_owner(splitted[1], username, groups, w_id, db).await;
} else if splitted[0] == "f" {
crate::folders::require_is_owner(authed, splitted[1])
} else {
Err(Error::BadRequest(format!(
"Not recognized path kind: {}",
path
)))
return crate::folders::require_is_owner(splitted[1], username, groups, w_id, db).await;
}
} else {
Err(Error::BadRequest(format!(
"Cannot be owner of an empty path"
)))
}
Err(Error::BadRequest(format!("not recognized owner kind")))
}
async fn whois(

View File

@@ -380,8 +380,9 @@ async fn update_variable(
if let Some(npath) = ns.path {
if npath != path {
check_path_conflict(&mut tx, &w_id, &npath).await?;
require_owner_of_path(&authed, path)?;
if !authed.is_admin {
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
}
let mut v = sqlx::query_scalar!(
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
path,

View File

@@ -1,162 +1 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
flow_status::FlowStatus,
flows::FlowValue,
scripts::{ScriptHash, ScriptLang},
};
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
#[sqlx(type_name = "JOB_KIND", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase"))]
pub enum JobKind {
Script,
#[allow(non_camel_case_types)]
Script_Hub,
Preview,
Dependencies,
Flow,
FlowPreview,
Identity,
FlowDependencies,
}
#[derive(Debug, sqlx::FromRow, Serialize, Clone)]
pub struct QueuedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_lock: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_ping: Option<chrono::DateTime<chrono::Utc>>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub same_worker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_run_error: Option<String>,
pub email: String,
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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_jobs: Option<serde_json::Value>,
}
impl QueuedJob {
pub fn script_path(&self) -> &str {
self.script_path
.as_ref()
.map(String::as_str)
.unwrap_or("tmp/main")
}
}
impl QueuedJob {
pub fn parse_raw_flow(&self) -> Option<FlowValue> {
self.raw_flow
.as_ref()
.and_then(|v| serde_json::from_value::<FlowValue>(v.clone()).ok())
}
pub fn parse_flow_status(&self) -> Option<FlowStatus> {
self.flow_status
.as_ref()
.and_then(|v| serde_json::from_value::<FlowStatus>(v.clone()).ok())
}
}
impl Default for QueuedJob {
fn default() -> Self {
Self {
workspace_id: "".to_string(),
id: Uuid::default(),
parent_job: None,
created_by: "".to_string(),
created_at: chrono::Utc::now(),
started_at: None,
scheduled_for: chrono::Utc::now(),
running: false,
script_hash: None,
script_path: None,
args: None,
logs: None,
raw_code: None,
raw_lock: None,
canceled: false,
canceled_by: None,
canceled_reason: None,
last_ping: None,
job_kind: JobKind::Identity,
schedule_path: None,
permissioned_as: "".to_string(),
flow_status: None,
raw_flow: None,
is_flow_step: false,
language: None,
same_worker: false,
pre_run_error: None,
email: "".to_string(),
visible_to_owner: false,
suspend: None,
mem_peak: None,
root_job: None,
leaf_jobs: None,
}
}
}
#[derive(Debug, Clone)]
pub enum JobPayload {
ScriptHub { path: String },
ScriptHash { hash: ScriptHash, path: String },
Code(RawCode),
Dependencies { hash: ScriptHash, dependencies: String, language: ScriptLang },
FlowDependencies { path: String },
Flow(String),
RawFlow { value: FlowValue, path: Option<String> },
Identity,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RawCode {
pub content: String,
pub path: Option<String>,
pub language: ScriptLang,
pub lock: Option<String>,
}

View File

@@ -15,7 +15,6 @@ pub mod error;
pub mod external_ip;
pub mod flow_status;
pub mod flows;
pub mod jobs;
pub mod more_serde;
pub mod oauth2;
pub mod schedule;

View File

@@ -10,6 +10,7 @@ use std::collections::HashMap;
use anyhow::Context;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres, Transaction};
use tracing::{instrument, Instrument};
use ulid::Ulid;
@@ -19,7 +20,6 @@ use windmill_common::{
error::{self, Error},
flow_status::{FlowStatus, JobResult, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL},
flows::{FlowModule, FlowModuleValue, FlowValue},
jobs::{JobKind, JobPayload, QueuedJob, RawCode},
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
utils::StripPath,
METRICS_ENABLED,
@@ -655,3 +655,117 @@ pub async fn get_hub_script(
.await
.map(|e| e)
}
#[derive(Debug, sqlx::FromRow, Serialize, Clone)]
pub struct QueuedJob {
pub workspace_id: String,
pub id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_job: Option<Uuid>,
pub created_by: String,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_hash: Option<ScriptHash>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_path: Option<String>,
pub args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logs: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_lock: Option<String>,
pub canceled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub canceled_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_ping: Option<chrono::DateTime<chrono::Utc>>,
pub job_kind: JobKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_path: Option<String>,
pub permissioned_as: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub flow_status: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_flow: Option<serde_json::Value>,
pub is_flow_step: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<ScriptLang>,
pub same_worker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_run_error: Option<String>,
pub email: String,
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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_jobs: Option<serde_json::Value>,
}
impl QueuedJob {
pub fn script_path(&self) -> &str {
self.script_path
.as_ref()
.map(String::as_str)
.unwrap_or("tmp/main")
}
}
impl QueuedJob {
pub fn parse_raw_flow(&self) -> Option<FlowValue> {
self.raw_flow
.as_ref()
.and_then(|v| serde_json::from_value::<FlowValue>(v.clone()).ok())
}
pub fn parse_flow_status(&self) -> Option<FlowStatus> {
self.flow_status
.as_ref()
.and_then(|v| serde_json::from_value::<FlowStatus>(v.clone()).ok())
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
#[sqlx(type_name = "JOB_KIND", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase"))]
pub enum JobKind {
Script,
#[allow(non_camel_case_types)]
Script_Hub,
Preview,
Dependencies,
Flow,
FlowPreview,
Identity,
FlowDependencies,
}
#[derive(Debug, Clone)]
pub enum JobPayload {
ScriptHub { path: String },
ScriptHash { hash: ScriptHash, path: String },
Code(RawCode),
Dependencies { hash: ScriptHash, dependencies: String, language: ScriptLang },
FlowDependencies { path: String },
Flow(String),
RawFlow { value: FlowValue, path: Option<String> },
Identity,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RawCode {
pub content: String,
pub path: Option<String>,
pub language: ScriptLang,
pub lock: Option<String>,
}

View File

@@ -6,10 +6,8 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::push;
use crate::QueueTransaction;
use crate::{push, JobPayload};
use sqlx::{query_scalar, Postgres, Transaction};
use windmill_common::jobs::JobPayload;
use std::str::FromStr;
use windmill_common::{
error::{self, Result},
@@ -17,6 +15,7 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::{now_from_db, StripPath},
};
use crate::{QueueTransaction};
pub async fn push_scheduled_job<'c, R: rsmq_async::RsmqConnection + Send + 'c>(
mut tx: QueueTransaction<'c, R>,

View File

@@ -1,41 +0,0 @@
use sqlx::{Pool, Postgres};
use tokio::{fs::File, io::AsyncReadExt};
use windmill_common::error::{self, Error};
use windmill_queue::CLOUD_HOSTED;
use crate::MAX_RESULT_SIZE;
pub 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}")))
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn set_logs(logs: &str, id: &uuid::Uuid, db: &Pool<Postgres>) {
if sqlx::query!(
"UPDATE queue SET logs = $1 WHERE id = $2",
logs.to_owned(),
id
)
.execute(db)
.await
.is_err()
{
tracing::error!(%id, "error updating logs for id {id}")
};
}
pub fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}

View File

@@ -1,249 +0,0 @@
#[cfg(feature = "enterprise")]
use crate::{
DENO_CACHE_DIR, DENO_TMP_CACHE_DIR, GO_CACHE_DIR, GO_TMP_CACHE_DIR, PIP_CACHE_DIR,
PIP_TMP_CACHE_DIR,
};
use crate::{ROOT_CACHE_DIR, ROOT_TMP_CACHE_DIR};
#[cfg(feature = "enterprise")]
use std::process::Stdio;
#[cfg(feature = "enterprise")]
use tokio::{fs::DirBuilder, process::Command, sync::mpsc::Sender, time::Instant};
use windmill_common::error;
#[cfg(feature = "enterprise")]
const TAR_CACHE_FILENAME: &str = "entirecache.tar";
#[cfg(feature = "enterprise")]
pub async fn copy_cache_from_bucket(
bucket: &str,
tx: Option<Sender<()>>,
) -> Option<tokio::task::JoinHandle<()>> {
tracing::info!("Copying cache from bucket in the background {bucket}");
let bucket = bucket.to_string();
let tx_is_some = tx.is_some();
let f = async move {
let elapsed = Instant::now();
match Command::new("rclone")
.arg("copy")
.arg(format!(":s3,env_auth=true:{bucket}"))
.arg(if tx_is_some {
ROOT_TMP_CACHE_DIR
} else {
ROOT_CACHE_DIR
})
.arg("--size-only")
.arg("--fast-list")
.arg("--exclude")
.arg(format!("\"{TAR_CACHE_FILENAME},/deno/gen/file/**\""))
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
h.wait().await.unwrap();
}
Err(e) => tracing::warn!("Failed to run periodic job pull. Error: {:?}", e),
}
tracing::info!(
"Finished copying cache from bucket {bucket}, took {:?}s",
elapsed.elapsed().as_secs()
);
for x in if !tx_is_some {
[PIP_CACHE_DIR, DENO_CACHE_DIR, GO_CACHE_DIR]
} else {
[PIP_TMP_CACHE_DIR, DENO_TMP_CACHE_DIR, GO_TMP_CACHE_DIR]
} {
DirBuilder::new()
.recursive(true)
.create(x)
.await
.expect("could not create initial worker dir");
}
if let Some(tx) = tx {
tx.send(()).await.expect("can send copy cache signal");
}
};
if tx_is_some {
return Some(tokio::spawn(f));
} else {
f.await;
return None;
}
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_to_bucket(bucket: &str) {
tracing::info!("Copying cache to bucket {bucket}");
let elapsed = Instant::now();
match Command::new("rclone")
.arg("copy")
.arg(ROOT_CACHE_DIR)
.arg(format!(":s3,env_auth=true:{bucket}"))
.arg("--size-only")
.arg("--fast-list")
.arg("--exclude")
.arg(format!("\"{TAR_CACHE_FILENAME},/deno/gen/file/**\""))
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
h.wait().await.unwrap();
}
Err(e) => tracing::info!("Failed to run periodic job push. Error: {:?}", e),
}
tracing::info!(
"Finished copying cache to bucket {bucket}, took: {:?}s",
elapsed.elapsed().as_secs()
);
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_to_bucket_as_tar(bucket: &str) {
tracing::info!("Copying cache to bucket {bucket} as tar");
let elapsed = Instant::now();
match Command::new("tar")
.current_dir(ROOT_CACHE_DIR)
.arg("-c")
.arg("-f")
.arg(format!("{ROOT_CACHE_DIR}{TAR_CACHE_FILENAME}"))
.args(&["pip", "go", "deno"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
if !h.wait().await.unwrap().success() {
tracing::info!("Failed to tar cache");
return;
}
}
Err(e) => {
tracing::info!("Failed tar cache. Error: {e:?}");
return;
}
}
let tar_metadata = tokio::fs::metadata(format!("{ROOT_CACHE_DIR}{TAR_CACHE_FILENAME}")).await;
if tar_metadata.is_err() || tar_metadata.as_ref().unwrap().len() == 0 {
tracing::info!("Failed to tar cache");
return;
}
match Command::new("rclone")
.current_dir(ROOT_CACHE_DIR)
.arg("copyto")
.arg(TAR_CACHE_FILENAME)
.arg(format!(":s3,env_auth=true:{bucket}/{TAR_CACHE_FILENAME}"))
.arg("-vv")
.arg("--size-only")
.arg("--fast-list")
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
h.wait().await.unwrap();
}
Err(e) => tracing::info!("Failed to copy tar cache to bucket. Error: {:?}", e),
}
if let Err(e) = tokio::fs::remove_file(format!("{ROOT_CACHE_DIR}{TAR_CACHE_FILENAME}")).await {
tracing::info!("Failed to remove tar cache. Error: {:?}", e);
};
tracing::info!(
"Finished copying cache to bucket {bucket} as tar, took: {:?}s. Size of new tar: {}",
elapsed.elapsed().as_secs(),
tar_metadata.unwrap().len()
);
}
#[cfg(feature = "enterprise")]
pub async fn copy_cache_from_bucket_as_tar(bucket: &str) -> bool {
tracing::info!("Copying cache from bucket {bucket} as tar");
let elapsed = Instant::now();
match Command::new("rclone")
.arg("copyto")
.arg(format!(":s3,env_auth=true:{bucket}/{TAR_CACHE_FILENAME}"))
.arg(format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}"))
.arg("-vv")
.arg("--size-only")
.arg("--fast-list")
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
if !h.wait().await.unwrap().success() {
tracing::info!("Failed to download tar cache, continuing nonetheless");
return false;
}
}
Err(e) => {
tracing::info!("Failed to download tar cache, continuing nonetheless. Error: {e:?}");
return false;
}
}
match Command::new("tar")
.current_dir(ROOT_TMP_CACHE_DIR)
.arg("-xpvf")
.arg(format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}"))
.stdin(Stdio::null())
.stdout(Stdio::null())
.spawn()
{
Ok(mut h) => {
if !h.wait().await.unwrap().success() {
tracing::info!("Failed to untar cache, continuing nonetheless");
return false;
}
}
Err(e) => {
tracing::warn!("Failed to untar cache, continuing nonetheless. Error: {e:?}");
return false;
}
}
if let Err(e) =
tokio::fs::remove_file(format!("{ROOT_TMP_CACHE_DIR}{TAR_CACHE_FILENAME}")).await
{
tracing::info!("Failed to remove tar cache. Error: {:?}", e);
};
let r = move_tmp_cache_to_cache().await.is_ok();
tracing::info!(
"Finished copying cache from bucket {bucket} as tar, took: {:?}s. copy success: {r}",
elapsed.elapsed().as_secs()
);
return r;
}
// async fn check_if_bucket_syncable(bucket: &str) -> bool {
// match Command::new("rclone")
// .arg("lsf")
// .arg(format!(":s3,env_auth=true:{bucket}/NOSYNC"))
// .arg("-vv")
// .arg("--fast-list")
// .stdin(Stdio::null())
// .stdout(Stdio::null())
// .output()
// .await;
// return true;
// }
pub async fn move_tmp_cache_to_cache() -> error::Result<()> {
tokio::fs::remove_dir_all(ROOT_CACHE_DIR).await?;
tokio::fs::rename(ROOT_TMP_CACHE_DIR, ROOT_CACHE_DIR).await?;
tracing::info!("Finished moving tmp cache to cache");
Ok(())
}

View File

@@ -1,362 +0,0 @@
use std::process::Stdio;
use itertools::Itertools;
use tokio::{
fs::{DirBuilder, File},
io::AsyncReadExt,
process::Command,
};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
utils::calculate_hash,
};
use windmill_parser_go::parse_go_imports;
use crate::{
common::{capitalize, read_result, set_logs},
create_args_and_out_file, get_reserved_variables, handle_child, write_file,
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, GOPRIVATE, GO_CACHE_DIR, HOME_ENV,
NETRC, NSJAIL_PATH, PATH_ENV,
};
const GO_REQ_SPLITTER: &str = "//go.sum\n";
const NSJAIL_CONFIG_RUN_GO_CONTENT: &str = include_str!("../nsjail/run.go.config.proto");
lazy_static::lazy_static! {
static ref GO_PATH: String = std::env::var("GO_PATH").unwrap_or_else(|_| "/usr/bin/go".to_string());
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_go_job(
logs: &mut String,
job: &QueuedJob,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
inner_content: &str,
job_dir: &str,
requirements_o: Option<String>,
shared_mount: &str,
base_internal_url: &str,
worker_name: &str,
) -> Result<serde_json::Value, Error> {
//go does not like executing modules at temp root
let job_dir = &format!("{job_dir}/go");
let (skip_go_mod, skip_tidy) = if let Some(requirements) = requirements_o {
gen_go_mod(inner_content, job_dir, &requirements).await?
} else {
(false, false)
};
logs.push_str("\n\n--- GO DEPENDENCIES SETUP ---\n");
set_logs(logs, &job.id, db).await;
install_go_dependencies(
&job.id,
inner_content,
logs,
job_dir,
db,
true,
skip_go_mod,
skip_tidy,
worker_name,
&job.workspace_id,
)
.await?;
logs.push_str("\n\n--- GO CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
let client = &client.get_authed().await;
create_args_and_out_file(client, job, job_dir).await?;
{
let sig = windmill_parser_go::parse_go_sig(&inner_content)?;
drop(inner_content);
const WRAPPER_CONTENT: &str = r#"package main
import (
"encoding/json"
"os"
"fmt"
"mymod/inner"
)
func main() {{
dat, err := os.ReadFile("args.json")
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
var req inner.Req
if err := json.Unmarshal(dat, &req); err != nil {{
fmt.Println(err)
os.Exit(1)
}}
res, err := inner.Run(req)
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
res_json, err := json.Marshal(res)
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
f, err := os.OpenFile("result.json", os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
_, err = f.WriteString(string(res_json))
if err != nil {{
fmt.Println(err)
os.Exit(1)
}}
}}"#;
write_file(job_dir, "main.go", WRAPPER_CONTENT).await?;
{
let spread = &sig
.args
.clone()
.into_iter()
.map(|x| format!("req.{}", capitalize(&x.name)))
.join(", ");
let req_body = &sig
.args
.into_iter()
.map(|x| {
format!(
"{} {} `json:\"{}\"`",
capitalize(&x.name),
windmill_parser_go::otyp_to_string(x.otyp),
x.name
)
})
.join("\n");
let runner_content: String = format!(
r#"package inner
type Req struct {{
{req_body}
}}
func Run(req Req) (interface{{}}, error){{
return main({spread})
}}
"#,
);
write_file(&format!("{job_dir}/inner"), "runner.go", &runner_content).await?;
}
}
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let child = if !*DISABLE_NSJAIL {
let _ = write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_GO_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", GO_CACHE_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount),
)
.await?;
let build_go = Command::new(GO_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("GOPATH", GO_CACHE_DIR)
.env("HOME", HOME_ENV.as_str())
.args(vec!["build", "main.go"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(
&job.id,
db,
logs,
build_go,
false,
worker_name,
&job.workspace_id,
)
.await?;
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec!["--config", "run.config.proto", "--", "/tmp/go/main"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
} else {
if let Some(ref netrc) = *NETRC {
write_file(&HOME_ENV, ".netrc", netrc).await?;
}
Command::new(GO_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("GOPATH", GO_CACHE_DIR)
.env("GOPRIVATE", GOPRIVATE.as_ref().unwrap_or(&String::new()))
.env("HOME", HOME_ENV.as_str())
.args(vec!["run", "main.go"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
handle_child(
&job.id,
db,
logs,
child,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
)
.await?;
read_result(job_dir).await
}
async fn gen_go_mod(
inner_content: &str,
job_dir: &str,
requirements: &str,
) -> error::Result<(bool, bool)> {
gen_go_mymod(inner_content, job_dir).await?;
let md = requirements.split_once(GO_REQ_SPLITTER);
if let Some((req, sum)) = md {
write_file(job_dir, "go.mod", &req).await?;
write_file(job_dir, "go.sum", &sum).await?;
Ok((true, true))
} else {
write_file(job_dir, "go.mod", &requirements).await?;
Ok((true, false))
}
}
pub async fn install_go_dependencies(
job_id: &Uuid,
code: &str,
logs: &mut String,
job_dir: &str,
db: &sqlx::Pool<sqlx::Postgres>,
non_dep_job: bool,
skip_go_mod: bool,
has_sum: bool,
worker_name: &str,
w_id: &str,
) -> error::Result<String> {
if !skip_go_mod {
gen_go_mymod(code, job_dir).await?;
let child = Command::new("go")
.current_dir(job_dir)
.args(vec!["mod", "init", "mymod"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name, w_id).await?;
}
let mut new_lockfile = false;
let hash = if !has_sum {
calculate_hash(parse_go_imports(&code)?.iter().join("\n").as_str())
} else {
"".to_string()
};
let mut skip_tidy = has_sum;
if !has_sum {
if let Some(cached) = sqlx::query_scalar!(
"SELECT lockfile FROM pip_resolution_cache WHERE hash = $1",
hash
)
.fetch_optional(db)
.await?
{
logs.push_str(&format!("\nfound cached resolution"));
gen_go_mod(code, job_dir, &cached).await?;
skip_tidy = true;
new_lockfile = false;
} else {
new_lockfile = true;
}
}
let mod_command = if skip_tidy { "download" } else { "tidy" };
let child = Command::new(GO_PATH.as_str())
.current_dir(job_dir)
.env("GOPATH", GO_CACHE_DIR)
.args(vec!["mod", mod_command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name, &w_id)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
if (!new_lockfile || has_sum) && non_dep_job {
return Ok("".to_string());
}
let mut req_content = "".to_string();
let mut file = File::open(format!("{job_dir}/go.mod")).await?;
file.read_to_string(&mut req_content).await?;
req_content.push_str(GO_REQ_SPLITTER);
let sum_path = format!("{job_dir}/go.sum");
if tokio::fs::metadata(&sum_path).await.is_ok() {
let mut file = File::open(sum_path).await?;
file.read_to_string(&mut req_content).await?;
}
if non_dep_job {
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
hash,
req_content
).fetch_optional(db).await?;
return Ok(String::new());
} else {
Ok(req_content)
}
}
async fn gen_go_mymod(code: &str, job_dir: &str) -> error::Result<()> {
let code = if code.trim_start().starts_with("package") {
code.to_string()
} else {
format!("package inner; {code}")
};
let mymod_dir = format!("{job_dir}/inner");
DirBuilder::new()
.recursive(true)
.create(&mymod_dir)
.await
.expect("could not create go's mymod dir");
write_file(&mymod_dir, "inner_main.go", &code).await?;
Ok(())
}

View File

@@ -10,13 +10,11 @@ use sqlx::{Pool, Postgres};
use tracing::instrument;
use uuid::Uuid;
use windmill_common::{
error::Error,
flow_status::FlowStatusModule,
jobs::{JobKind, QueuedJob},
schedule::Schedule,
METRICS_ENABLED,
error::Error, flow_status::FlowStatusModule, schedule::Schedule, METRICS_ENABLED,
};
use windmill_queue::{
delete_job, schedule::get_schedule_opt, JobKind, QueueTransaction, QueuedJob, CLOUD_HOSTED,
};
use windmill_queue::{delete_job, schedule::get_schedule_opt, QueueTransaction, CLOUD_HOSTED};
#[instrument(level = "trace", skip_all)]
pub async fn add_completed_job_error<R: rsmq_async::RsmqConnection + Clone + Send>(

View File

@@ -1,9 +1,5 @@
mod common;
mod global_cache;
mod go_executor;
mod jobs;
mod js_eval;
mod python_executor;
mod worker;
mod worker_flow;

View File

@@ -0,0 +1,92 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
// use std::{net::SocketAddr, time::Duration};
// use anyhow::Context;
// use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
// use windmill_common::{
// error::{self, Error},
// utils::rd_string,
// };
// #[tokio::main]
// async fn main() -> anyhow::Result<()> {
// // dotenv().ok();
// windmill_common::tracing_init::initialize_tracing();
// let db = async {
// let database_url = std::env::var("DATABASE_URL")
// .map_err(|_| Error::BadConfig("DATABASE_URL env var is missing".to_string()))?;
// let max_connections = match std::env::var("DATABASE_CONNECTIONS") {
// Ok(n) => n.parse::<u32>().context("invalid DATABASE_CONNECTIONS")?,
// Err(_) => 10,
// };
// Ok::<Pool<Postgres>, error::Error>(
// PgPoolOptions::new()
// .max_connections(max_connections)
// .max_lifetime(Duration::from_secs(30 * 60)) // 30 mins
// .connect(&database_url)
// .await
// .map_err(|err| Error::ConnectingToDatabase(err.to_string()))?,
// )
// }
// .await?;
// let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
// .ok()
// .map(|s| {
// s.parse::<bool>()
// .map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
// .or_else(|_| s.parse::<SocketAddr>().map(Some))
// })
// .transpose()?
// .flatten();
// let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
// let shutdown_signal = windmill_common::shutdown_signal(tx);
// let workers_f = async {
// let instance_name = rd_string(5);
// let ip = windmill_common::external_ip::get_ip()
// .await
// .unwrap_or_else(|e| {
// tracing::warn!(error = e.to_string(), "failed to get external IP");
// "unretrievable IP".to_string()
// });
// let worker_name = format!("dt-worker-{}-{}", &instance_name, rd_string(5));
// windmill_worker::run_worker(
// &db.clone(),
// &instance_name,
// worker_name,
// 1,
// 1,
// &ip,
// rx.resubscribe(),
// )
// .await;
// Ok(()) as anyhow::Result<()>
// };
// let metrics_f = async {
// match metrics_addr {
// Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
// .await
// .map_err(anyhow::Error::from),
// None => Ok(()),
// }
// };
// futures::try_join!(shutdown_signal, workers_f, metrics_f)?;
// Ok(())
// }

View File

@@ -1,535 +0,0 @@
use std::process::Stdio;
use itertools::Itertools;
use regex::Regex;
use sqlx::{Pool, Postgres};
use tokio::{
fs::{metadata, DirBuilder, File},
io::AsyncReadExt,
process::Command,
};
use uuid::Uuid;
use windmill_common::{
error::{self, Error},
jobs::QueuedJob,
utils::calculate_hash,
};
lazy_static::lazy_static! {
static ref PYTHON_PATH: String =
std::env::var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
static ref PIP_INDEX_URL: Option<String> = std::env::var("PIP_INDEX_URL").ok();
static ref PIP_EXTRA_INDEX_URL: Option<String> = std::env::var("PIP_EXTRA_INDEX_URL").ok();
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
static ref PIP_LOCAL_DEPENDENCIES: Option<Vec<String>> = {
let pip_local_dependencies = std::env::var("PIP_LOCAL_DEPENDENCIES")
.ok()
.map(|x| x.split(',').map(|x| x.to_string()).collect());
if pip_local_dependencies == Some(vec!["".to_string()]) {
None
} else {
pip_local_dependencies
}
};
static ref ADDITIONAL_PYTHON_PATHS: Option<Vec<String>> = std::env::var("ADDITIONAL_PYTHON_PATHS")
.ok()
.map(|x| x.split(':').map(|x| x.to_string()).collect());
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
}
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
const RELATIVE_PYTHON_LOADER: &str = include_str!("../loader.py");
use crate::{
common::{read_result, set_logs},
create_args_and_out_file, get_reserved_variables, handle_child, write_file,
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, NSJAIL_PATH, PATH_ENV,
PIP_CACHE_DIR,
};
pub async fn create_dependencies_dir(job_dir: &str) {
DirBuilder::new()
.recursive(true)
.create(&format!("{job_dir}/dependencies"))
.await
.expect("could not create dependencies dir");
}
pub async fn pip_compile(
job_id: &Uuid,
requirements: &str,
logs: &mut String,
job_dir: &str,
db: &Pool<Postgres>,
worker_name: &str,
w_id: &str,
) -> error::Result<String> {
logs.push_str(&format!("\nresolving dependencies..."));
set_logs(logs, job_id, db).await;
logs.push_str(&format!("\ncontent of requirements:\n{}", requirements));
let req_hash = calculate_hash(&requirements);
if let Some(cached) = sqlx::query_scalar!(
"SELECT lockfile FROM pip_resolution_cache WHERE hash = $1",
req_hash
)
.fetch_optional(db)
.await?
{
logs.push_str(&format!("\nfound cached resolution"));
return Ok(cached);
}
let file = "requirements.in";
let requirements = if let Some(pip_local_dependencies) = PIP_LOCAL_DEPENDENCIES.as_ref() {
let deps = pip_local_dependencies.clone();
requirements
.lines()
.filter(|s| !deps.contains(&s.to_string()))
.join("\n")
} else {
requirements.to_string()
};
write_file(job_dir, file, &requirements).await?;
let mut args = vec!["-q", "--no-header", file, "--resolver=backtracking"];
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
args.extend(["--extra-index-url", url]);
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
args.extend(["--index-url", url]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
let child = Command::new("pip-compile")
.current_dir(job_dir)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
handle_child(job_id, db, logs, child, false, worker_name, &w_id)
.await
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
let path_lock = format!("{job_dir}/requirements.txt");
let mut file = File::open(path_lock).await?;
let mut req_content = "".to_string();
file.read_to_string(&mut req_content).await?;
let lockfile = req_content
.lines()
.filter(|x| !x.trim_start().starts_with('#'))
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("\n");
sqlx::query!(
"INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2",
req_hash,
lockfile
).fetch_optional(db).await?;
Ok(lockfile)
}
#[tracing::instrument(level = "trace", skip_all)]
pub async fn handle_python_job(
requirements_o: Option<String>,
job_dir: &str,
worker_dir: &str,
worker_name: &str,
job: &QueuedJob,
logs: &mut String,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClientBackgroundTask,
inner_content: &String,
shared_mount: &str,
base_internal_url: &str,
) -> windmill_common::error::Result<serde_json::Value> {
create_dependencies_dir(job_dir).await;
let mut additional_python_paths: Vec<String> =
ADDITIONAL_PYTHON_PATHS.to_owned().unwrap_or_else(|| vec![]);
let requirements = match requirements_o {
Some(r) => r,
None => {
let requirements = windmill_parser_py::parse_python_imports(&inner_content)?.join("\n");
if requirements.is_empty() {
"".to_string()
} else {
pip_compile(
&job.id,
&requirements,
logs,
job_dir,
db,
worker_name,
&job.workspace_id,
)
.await
.map_err(|e| {
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
})?
}
}
};
if requirements.len() > 0 {
if !*DISABLE_NSJAIL {
let _ = write_file(
job_dir,
"download.config.proto",
&NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT
.replace("{WORKER_DIR}", &worker_dir)
.replace("{CACHE_DIR}", PIP_CACHE_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string()),
)
.await?;
}
additional_python_paths = handle_python_reqs(
requirements
.split("\n")
.filter(|x| !x.starts_with("--"))
.collect(),
&job.id,
&job.workspace_id,
logs,
db,
worker_name,
job_dir,
)
.await?;
}
logs.push_str("\n\n--- PYTHON CODE EXECUTION ---\n");
set_logs(logs, &job.id, db).await;
let relative_imports = RELATIVE_IMPORT_REGEX.is_match(&inner_content);
let script_path_splitted = &job.script_path().split("/");
let dirs_full = script_path_splitted
.clone()
.take(script_path_splitted.clone().count() - 1)
.join("/")
.replace("-", "_");
let dirs = if dirs_full.len() > 0 {
dirs_full
} else {
"tmp".to_string()
};
let last = script_path_splitted
.clone()
.last()
.unwrap()
.replace("-", "_")
.replace(" ", "_")
.to_lowercase();
let module_dir = format!("{}/{}", job_dir, dirs);
tokio::fs::create_dir_all(format!("{module_dir}/")).await?;
let _ = write_file(&module_dir, &format!("{last}.py"), inner_content).await?;
if relative_imports {
let _ = write_file(&job_dir, "loader.py", RELATIVE_PYTHON_LOADER).await?;
}
let sig = windmill_parser_py::parse_python_signature(inner_content)?;
let transforms = sig
.args
.iter()
.map(|x| match x.typ {
windmill_parser::Typ::Bytes => {
format!(
"if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n \
kwargs[\"{}\"] = base64.b64decode(kwargs[\"{}\"])\n",
x.name, x.name, x.name, x.name
)
}
windmill_parser::Typ::Datetime => {
format!(
"if \"{}\" in kwargs and kwargs[\"{}\"] is not None:\n \
kwargs[\"{}\"] = datetime.strptime(kwargs[\"{}\"], \
'%Y-%m-%dT%H:%M')\n",
x.name, x.name, x.name, x.name
)
}
_ => "".to_string(),
})
.collect::<Vec<String>>()
.join("");
let client = client.get_authed().await;
create_args_and_out_file(&client, job, job_dir).await?;
let import_loader = if relative_imports {
"import loader"
} else {
""
};
let import_base64 = if sig
.args
.iter()
.any(|x| x.typ == windmill_parser::Typ::Bytes)
{
"import base64"
} else {
""
};
let import_datetime = if sig
.args
.iter()
.any(|x| x.typ == windmill_parser::Typ::Datetime)
{
"from datetime import datetime"
} else {
""
};
let spread = if sig.star_kwargs {
"args = kwargs".to_string()
} else {
sig.args
.into_iter()
.map(|x| format!("args[\"{}\"] = kwargs.get(\"{}\")", x.name, x.name))
.join("\n")
};
let module_dir_dot = dirs.replace("/", ".").replace("-", "_");
let wrapper_content: String = format!(
r#"
import json
{import_loader}
{import_base64}
{import_datetime}
import traceback
import sys
from {module_dir_dot} import {last} as inner_script
with open("args.json") as f:
kwargs = json.load(f, strict=False)
args = {{}}
{spread}
{transforms}
for k, v in list(args.items()):
if v == '<function call>':
del args[k]
try:
res = inner_script.main(**args)
typ = type(res)
if typ.__name__ == 'DataFrame':
if typ.__module__ == 'pandas.core.frame':
res = res.values.tolist()
elif typ.__module__ == 'polars.dataframe.frame':
res = res.rows()
res_json = json.dumps(res, separators=(',', ':'), default=str).replace('\n', '')
with open("result.json", 'w') as f:
f.write(res_json)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
tb = traceback.format_tb(exc_traceback)
with open("result.json", 'w') as f:
err_json = json.dumps({{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}, separators=(',', ':'), default=str).replace('\n', '')
f.write(err_json)
sys.exit(1)
"#,
);
write_file(job_dir, "wrapper.py", &wrapper_content).await?;
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let additional_python_paths_folders = additional_python_paths.iter().join(":");
if !*DISABLE_NSJAIL {
let shared_deps = additional_python_paths
.into_iter()
.map(|pp| {
format!(
r#"
mount {{
src: "{pp}"
dst: "{pp}"
is_bind: true
rw: false
}}
"#
)
})
.join("\n");
let _ = write_file(
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_PYTHON3_CONTENT
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
.replace("{MAIN}", format!("{dirs}/{last}").as_str())
.replace(
"{ADDITIONAL_PYTHON_PATHS}",
additional_python_paths_folders.as_str(),
),
)
.await?;
} else {
reserved_variables.insert("PYTHONPATH".to_string(), additional_python_paths_folders);
}
tracing::info!(
worker_name = %worker_name,
job_id = %job.id,
workspace_id = %job.workspace_id,
"started python code execution {}",
job.id
);
let child = if !*DISABLE_NSJAIL {
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
.env_clear()
// inject PYTHONPATH here - for some reason I had to do it in nsjail conf
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec![
"--config",
"run.config.proto",
"--",
PYTHON_PATH.as_str(),
"-u",
"-m",
"wrapper",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
} else {
Command::new(PYTHON_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(vec!["-u", "-m", "wrapper"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
handle_child(
&job.id,
db,
logs,
child,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
)
.await?;
read_result(job_dir).await
}
pub async fn handle_python_reqs(
requirements: Vec<&str>,
job_id: &Uuid,
w_id: &str,
logs: &mut String,
db: &sqlx::Pool<sqlx::Postgres>,
worker_name: &str,
job_dir: &str,
) -> error::Result<Vec<String>> {
let mut req_paths: Vec<String> = vec![];
let mut vars = vec![("PATH", PATH_ENV.as_str())];
if !*DISABLE_NSJAIL {
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
vars.push(("EXTRA_INDEX_URL", url));
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
vars.push(("INDEX_URL", url));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
vars.push(("TRUSTED_HOST", host));
}
};
for req in requirements {
// todo: handle many reqs
let venv_p = format!("{PIP_CACHE_DIR}/{req}");
if metadata(&venv_p).await.is_ok() {
req_paths.push(venv_p);
continue;
}
logs.push_str("\n--- PIP INSTALL ---\n");
logs.push_str(&format!("\n{req} is being installed for the first time.\n It will be cached for all ulterior uses."));
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"started setup python dependencies"
);
let child = if !*DISABLE_NSJAIL {
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
"starting nsjail"
);
let mut vars = vars.clone();
let req = req.to_string();
vars.push(("REQ", &req));
vars.push(("TARGET", &venv_p));
Command::new(NSJAIL_PATH.as_str())
.current_dir(job_dir)
.env_clear()
.envs(vars)
.args(vec!["--config", "download.config.proto"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
} else {
let mut args = vec![
"-m",
"pip",
"install",
&req,
"-I",
"--no-deps",
"--no-color",
"--isolated",
"--no-warn-conflicts",
"--disable-pip-version-check",
"-t",
venv_p.as_str(),
];
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
args.extend(["--extra-index-url", url]);
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
args.extend(["--index-url", url]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", &host]);
}
Command::new(PYTHON_PATH.as_str())
.env_clear()
.env("PATH", PATH_ENV.as_str())
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
};
let child = handle_child(&job_id, db, logs, child, false, worker_name, &w_id).await;
tracing::info!(
worker_name = %worker_name,
job_id = %job_id,
workspace_id = %w_id,
is_ok = child.is_ok(),
"finished setting up python dependencies {}",
job_id
);
child?;
req_paths.push(venv_p);
}
Ok(req_paths)
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,6 @@ use tokio::sync::mpsc::Sender;
use tracing::instrument;
use uuid::Uuid;
use windmill_common::flow_status::{FlowStatusModuleWParent, Iterator, JobResult};
use windmill_common::jobs::{QueuedJob, JobPayload, RawCode};
use windmill_common::{
error::{self, to_anyhow, Error},
flow_status::{
@@ -31,7 +30,9 @@ use windmill_common::{
type DB = sqlx::Pool<sqlx::Postgres>;
use windmill_queue::{canceled_job_to_result, get_queued_job, push, QueueTransaction};
use windmill_queue::{
canceled_job_to_result, get_queued_job, push, JobPayload, QueueTransaction, QueuedJob, RawCode,
};
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion<

View File

@@ -19,7 +19,7 @@ import sync from "./sync.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
export const VERSION = "v1.88.0";
export const VERSION = "v1.87.0";
let command: any = new Command()
.name("wmill")

86
docker-compose-dev.yml Normal file
View File

@@ -0,0 +1,86 @@
version: "3.7"
services:
db:
image: postgres:14
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
expose:
- 5432
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: windmill
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
windmill_server:
image: ghcr.io/windmill-labs/windmill:main
deploy:
replicas: 1
restart: unless-stopped
expose:
- 8000
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
# volumes:
# - ./oauth.json/:/usr/src/app/oauth.json
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://windmill_server:8000
- RUST_LOG=info
- NUM_WORKERS=1
- DISABLE_SERVER=true
- KEEP_JOB_DIR=false
- DENO_PATH=/usr/bin/deno
- PYTHON_PATH=/usr/local/bin/python3
- METRICS_ADDR=false
depends_on:
db:
condition: service_healthy
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
- worker_dependency_cache:/tmp/windmill/cache
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest
restart: unless-stopped
expose:
- 3001
caddy:
image: caddy:2.5.2-alpine
restart: unless-stopped
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
ports:
- 80:80
- 443:443
environment:
- BASE_URL=${WM_BASE_URL}
volumes:
db_data: null
worker_dependency_cache: null

3
frontend/.gitignore vendored
View File

@@ -7,5 +7,4 @@ node_modules
CaddyfileRemoteRuben
tests-out/
storageState.json
.env.production
dist/
.env.production

View File

@@ -17,7 +17,7 @@ In the root folder:
```bash
docker build . -t windmill
docker compose up db windmill_server windmill_worker
docker compose -f docker-compose-dev.yml up db windmill_server windmill_worker
```
### 2. Backend is run by cargo
@@ -91,7 +91,7 @@ docker-compose up db
In the backend folder:
```bash
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5433/windmill?sslmode=disable cargo run
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable cargo run
```
You can now access [http://127.0.0.1:8000](http://127.0.0.1:8000).

View File

@@ -1,20 +1,19 @@
{
"name": "windmill",
"version": "1.88.0",
"version": "1.87.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.88.0",
"license": "Apache-2.0",
"version": "1.87.0",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@leeoniya/ufuzzy": "^1.0.6",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.8.5",
"@tanstack/svelte-table": "^8.7.6",
"ag-grid-svelte": "^0.1.4",
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^3.0.0",
@@ -39,9 +38,9 @@
"devDependencies": {
"@playwright/test": "^1.31.1",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.15.6",
"@sveltejs/package": "^2.0.2",
"@sveltejs/adapter-static": "^2.0.1",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
@@ -50,7 +49,7 @@
"@types/node": "^18.11.18",
"@types/vscode": "~1.77.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.59.0",
"@typescript-eslint/parser": "^5.48.0",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^6.0.0",
@@ -58,7 +57,7 @@
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"monaco-editor-workers": "0.37.0",
"monaco-editor-workers": "0.36.0",
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
@@ -69,11 +68,11 @@
"prettier-plugin-svelte": "^2.9.0",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
"svelte-awesome": "^3.2.0",
"svelte-awesome-color-picker": "^2.4.1",
"svelte-check": "^3.2.0",
"svelte-check": "^3.0.2",
"svelte-highlight": "^7.2.1",
"svelte-multiselect": "^8.6.0",
"svelte-overlay": "^1.4.1",
@@ -84,12 +83,11 @@
"svelte2tsx": "^0.6.11",
"tailwindcss": "^3.2.2",
"tslib": "^2.4.1",
"typescript": "^5.0.4",
"typescript": "^4.9.3",
"vite": "^4.0.4",
"yootils": "^0.3.1"
},
"peerDependencies": {
"@sveltejs/kit": "^1.15.6",
"svelte": "^3.55.1"
}
},
@@ -221,55 +219,6 @@
"node": ">=4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.1.tgz",
"integrity": "sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==",
"dev": true,
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^2.1.1"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz",
"integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==",
"dev": true,
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
},
"node_modules/@csstools/media-query-list-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz",
"integrity": "sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==",
"dev": true,
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/csstools"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^2.1.1",
"@csstools/css-tokenizer": "^2.1.1"
}
},
"node_modules/@csstools/selector-specificity": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz",
@@ -964,18 +913,18 @@
}
},
"node_modules/@sveltejs/adapter-static": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.2.tgz",
"integrity": "sha512-9wYtf6s6ew7DHUHMrt55YpD1FgV7oWql2IGsW5BXquLxqcY9vjrqCFo0TzzDpo+ZPZkW/v77k0eOP6tsAb8HmQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.1.tgz",
"integrity": "sha512-o5/q3YwD/ErxYCFlK1v3ydvldyNKk1lh3oeyxn4mhz+Pkbx/uuxhzmbOpytTlp5aVqNHDVsb04xadUzOFCDDzw==",
"dev": true,
"peerDependencies": {
"@sveltejs/kit": "^1.5.0"
}
},
"node_modules/@sveltejs/kit": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.6.tgz",
"integrity": "sha512-nXAVKq4veZqztZbPRDMpZszerSuZk3dMZmIoYc/6YvctYYkZS0cdQy5qfeGQ404wXRonCpaCQb0lUrdJZ81f9A==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.2.tgz",
"integrity": "sha512-rLNxZrjbrlPf8AWW8GAU4L/Vvu17e9v8EYl7pUip7x72lTft7RcxeP3z7tsrHpMSBBxC9o4XdKzFvz1vMZyXZw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -1005,9 +954,9 @@
}
},
"node_modules/@sveltejs/package": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.0.2.tgz",
"integrity": "sha512-cCOCcO8yMHnhHyaR51nQtvKZ3o/vSU9UYI1EXLT1j2CKNPMuH1/g6JNwKcNNrtQGwwquudc69ZeYy8D/TDNwEw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-1.0.2.tgz",
"integrity": "sha512-VY9U+05d9uNFDj7ScKRlHORYlfPSHwJewBjV+V2RsnViexpLFPUrboC9SiPYDCpLnbeqwXerxhO6twGHUBGeIA==",
"dev": true,
"dependencies": {
"chokidar": "^3.5.3",
@@ -1074,11 +1023,11 @@
}
},
"node_modules/@tanstack/svelte-table": {
"version": "8.8.5",
"resolved": "https://registry.npmjs.org/@tanstack/svelte-table/-/svelte-table-8.8.5.tgz",
"integrity": "sha512-d33JJwEe5AbwH6sBnRDfTL3gianA7if0fUTqxLNKK6P51IAIgMgAW+ahbCeg+5wJ9KcMSyTEOHs2Fg03oP6tvA==",
"version": "8.8.4",
"resolved": "https://registry.npmjs.org/@tanstack/svelte-table/-/svelte-table-8.8.4.tgz",
"integrity": "sha512-hI8DU5LhA16h649H25+at9m0oY09iMuTQNX+Ew/C9HX84FLhEHYWNH3DMY9rlNKEzzYH9EXCo0hmjU+wWUZ42g==",
"dependencies": {
"@tanstack/table-core": "8.8.5"
"@tanstack/table-core": "8.8.4"
},
"engines": {
"node": ">=12"
@@ -1092,9 +1041,9 @@
}
},
"node_modules/@tanstack/table-core": {
"version": "8.8.5",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.8.5.tgz",
"integrity": "sha512-Xnwa1qxpgvSW7ozLiexmKp2PIYcLBiY/IizbdGriYCL6OOHuZ9baRhrrH51zjyz+61ly6K59rmt6AI/3RR+97Q==",
"version": "8.8.4",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.8.4.tgz",
"integrity": "sha512-4gq0A/Qz/JFuclxPd1UnNi5P57vbVF10mPK743Iuv2ORLLgF/O/XkaYjYDGw5SHMof8CgiT/nfdJdlgHsOfRIg==",
"engines": {
"node": ">=12"
},
@@ -1409,6 +1358,13 @@
"dev": true,
"peer": true
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true,
"peer": true
},
"node_modules/@types/pug": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz",
@@ -1462,14 +1418,14 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.0.tgz",
"integrity": "sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==",
"version": "5.57.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz",
"integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.59.0",
"@typescript-eslint/types": "5.59.0",
"@typescript-eslint/typescript-estree": "5.59.0",
"@typescript-eslint/scope-manager": "5.57.0",
"@typescript-eslint/types": "5.57.0",
"@typescript-eslint/typescript-estree": "5.57.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1488,80 +1444,6 @@
}
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz",
"integrity": "sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.59.0",
"@typescript-eslint/visitor-keys": "5.59.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.0.tgz",
"integrity": "sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz",
"integrity": "sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.59.0",
"@typescript-eslint/visitor-keys": "5.59.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz",
"integrity": "sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "5.59.0",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "5.57.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.57.0.tgz",
@@ -2274,22 +2156,20 @@
}
},
"node_modules/cosmiconfig": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz",
"integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dev": true,
"peer": true,
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"parse-json": "^5.0.0",
"path-type": "^4.0.0"
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
"node": ">=10"
}
},
"node_modules/cross-spawn": {
@@ -3641,9 +3521,9 @@
}
},
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
"integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz",
"integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==",
"dev": true,
"peer": true,
"engines": {
@@ -3998,9 +3878,9 @@
}
},
"node_modules/known-css-properties": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.27.0.tgz",
"integrity": "sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==",
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz",
"integrity": "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==",
"dev": true,
"peer": true
},
@@ -4393,9 +4273,9 @@
"integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg=="
},
"node_modules/monaco-editor-workers": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.37.0.tgz",
"integrity": "sha512-mawGzbN6PNumIME4EUXLokDHslL0Rn7gcRxLZdR+TvTpyihhQaSQFgAwZTCvNIIvNjeQH5FALzX3WFPnaUhwIA==",
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.36.0.tgz",
"integrity": "sha512-byrccv+VN5MC9aAjLd4wGis1cPVYtaxvBRjlqupiv8BrNehuPqRlc+T3j1A6Ow62j5AAHxnzHtWkIwPvDDmYIg==",
"dev": true
},
"node_modules/monaco-languageclient": {
@@ -6292,21 +6172,17 @@
}
},
"node_modules/stylelint": {
"version": "15.5.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.5.0.tgz",
"integrity": "sha512-jyMO3R1QtE5mUS4v40+Gg+sIQBqe7CF1xPslxycDzNVkIBCUD4O+5F1vLPq16VmunUTv4qG9o2rUKLnU5KkVeQ==",
"version": "14.16.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz",
"integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==",
"dev": true,
"peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^2.1.0",
"@csstools/css-tokenizer": "^2.1.0",
"@csstools/media-query-list-parser": "^2.0.2",
"@csstools/selector-specificity": "^2.2.0",
"@csstools/selector-specificity": "^2.0.2",
"balanced-match": "^2.0.0",
"colord": "^2.9.3",
"cosmiconfig": "^8.1.3",
"cosmiconfig": "^7.1.0",
"css-functions-list": "^3.1.0",
"css-tree": "^2.3.1",
"debug": "^4.3.4",
"fast-glob": "^3.2.12",
"fastest-levenshtein": "^1.0.16",
@@ -6314,18 +6190,18 @@
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.3.1",
"ignore": "^5.2.4",
"html-tags": "^3.2.0",
"ignore": "^5.2.1",
"import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
"known-css-properties": "^0.27.0",
"known-css-properties": "^0.26.0",
"mathml-tag-names": "^2.1.3",
"meow": "^9.0.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.21",
"postcss": "^8.4.19",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0",
@@ -6335,17 +6211,17 @@
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"style-search": "^0.1.0",
"supports-hyperlinks": "^3.0.0",
"supports-hyperlinks": "^2.3.0",
"svg-tags": "^1.0.0",
"table": "^6.8.1",
"v8-compile-cache": "^2.3.0",
"write-file-atomic": "^5.0.0"
"write-file-atomic": "^4.0.2"
},
"bin": {
"stylelint": "bin/stylelint.js"
},
"engines": {
"node": "^14.13.1 || >=16.0.0"
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
@@ -6353,12 +6229,12 @@
}
},
"node_modules/stylelint-config-recommended": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-12.0.0.tgz",
"integrity": "sha512-x6x8QNARrGO2sG6iURkzqL+Dp+4bJorPMMRNPScdvaUK8PsynriOcMW7AFDKqkWAS5wbue/u8fUT/4ynzcmqdQ==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz",
"integrity": "sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==",
"dev": true,
"peerDependencies": {
"stylelint": "^15.5.0"
"stylelint": "^14.10.0"
}
},
"node_modules/stylelint/node_modules/balanced-match": {
@@ -6455,9 +6331,9 @@
}
},
"node_modules/supports-hyperlinks": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz",
"integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
"integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
"dev": true,
"peer": true,
"dependencies": {
@@ -6465,7 +6341,7 @@
"supports-color": "^7.0.0"
},
"engines": {
"node": ">=14.18"
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
@@ -6524,9 +6400,9 @@
}
},
"node_modules/svelte-check": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.2.0.tgz",
"integrity": "sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.1.4.tgz",
"integrity": "sha512-25Lb46ZS4IK/XpBMe4IBMrtYf23V8alqBX+szXoccb7uM0D2Wqq5rMRzYBONZnFVuU1bQG3R50lyIT5eRewv2g==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -6535,8 +6411,8 @@
"import-fresh": "^3.2.1",
"picocolors": "^1.0.0",
"sade": "^1.7.4",
"svelte-preprocess": "^5.0.3",
"typescript": "^5.0.3"
"svelte-preprocess": "^5.0.0",
"typescript": "^4.9.4"
},
"bin": {
"svelte-check": "bin/svelte-check"
@@ -7097,16 +6973,16 @@
}
},
"node_modules/typescript": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=12.20"
"node": ">=4.2.0"
}
},
"node_modules/uglify-js": {
@@ -7435,9 +7311,9 @@
"dev": true
},
"node_modules/write-file-atomic": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.0.tgz",
"integrity": "sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"peer": true,
"dependencies": {
@@ -7445,7 +7321,7 @@
"signal-exit": "^3.0.7"
},
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/xml-utils": {
@@ -7604,29 +7480,6 @@
}
}
},
"@csstools/css-parser-algorithms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.1.1.tgz",
"integrity": "sha512-viRnRh02AgO4mwIQb2xQNJju0i+Fh9roNgmbR5xEuG7J3TGgxjnE95HnBLgsFJOJOksvcfxOUCgODcft6Y07cA==",
"dev": true,
"peer": true,
"requires": {}
},
"@csstools/css-tokenizer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz",
"integrity": "sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==",
"dev": true,
"peer": true
},
"@csstools/media-query-list-parser": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.0.4.tgz",
"integrity": "sha512-GyYot6jHgcSDZZ+tLSnrzkR7aJhF2ZW6d+CXH66mjy5WpAQhZD4HDke2OQ36SivGRWlZJpAz7TzbW6OKlEpxAA==",
"dev": true,
"peer": true,
"requires": {}
},
"@csstools/selector-specificity": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz",
@@ -8036,16 +7889,16 @@
"requires": {}
},
"@sveltejs/adapter-static": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.2.tgz",
"integrity": "sha512-9wYtf6s6ew7DHUHMrt55YpD1FgV7oWql2IGsW5BXquLxqcY9vjrqCFo0TzzDpo+ZPZkW/v77k0eOP6tsAb8HmQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.1.tgz",
"integrity": "sha512-o5/q3YwD/ErxYCFlK1v3ydvldyNKk1lh3oeyxn4mhz+Pkbx/uuxhzmbOpytTlp5aVqNHDVsb04xadUzOFCDDzw==",
"dev": true,
"requires": {}
},
"@sveltejs/kit": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.6.tgz",
"integrity": "sha512-nXAVKq4veZqztZbPRDMpZszerSuZk3dMZmIoYc/6YvctYYkZS0cdQy5qfeGQ404wXRonCpaCQb0lUrdJZ81f9A==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.15.2.tgz",
"integrity": "sha512-rLNxZrjbrlPf8AWW8GAU4L/Vvu17e9v8EYl7pUip7x72lTft7RcxeP3z7tsrHpMSBBxC9o4XdKzFvz1vMZyXZw==",
"dev": true,
"requires": {
"@sveltejs/vite-plugin-svelte": "^2.0.0",
@@ -8064,9 +7917,9 @@
}
},
"@sveltejs/package": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-2.0.2.tgz",
"integrity": "sha512-cCOCcO8yMHnhHyaR51nQtvKZ3o/vSU9UYI1EXLT1j2CKNPMuH1/g6JNwKcNNrtQGwwquudc69ZeYy8D/TDNwEw==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/package/-/package-1.0.2.tgz",
"integrity": "sha512-VY9U+05d9uNFDj7ScKRlHORYlfPSHwJewBjV+V2RsnViexpLFPUrboC9SiPYDCpLnbeqwXerxhO6twGHUBGeIA==",
"dev": true,
"requires": {
"chokidar": "^3.5.3",
@@ -8111,17 +7964,17 @@
}
},
"@tanstack/svelte-table": {
"version": "8.8.5",
"resolved": "https://registry.npmjs.org/@tanstack/svelte-table/-/svelte-table-8.8.5.tgz",
"integrity": "sha512-d33JJwEe5AbwH6sBnRDfTL3gianA7if0fUTqxLNKK6P51IAIgMgAW+ahbCeg+5wJ9KcMSyTEOHs2Fg03oP6tvA==",
"version": "8.8.4",
"resolved": "https://registry.npmjs.org/@tanstack/svelte-table/-/svelte-table-8.8.4.tgz",
"integrity": "sha512-hI8DU5LhA16h649H25+at9m0oY09iMuTQNX+Ew/C9HX84FLhEHYWNH3DMY9rlNKEzzYH9EXCo0hmjU+wWUZ42g==",
"requires": {
"@tanstack/table-core": "8.8.5"
"@tanstack/table-core": "8.8.4"
}
},
"@tanstack/table-core": {
"version": "8.8.5",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.8.5.tgz",
"integrity": "sha512-Xnwa1qxpgvSW7ozLiexmKp2PIYcLBiY/IizbdGriYCL6OOHuZ9baRhrrH51zjyz+61ly6K59rmt6AI/3RR+97Q=="
"version": "8.8.4",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.8.4.tgz",
"integrity": "sha512-4gq0A/Qz/JFuclxPd1UnNi5P57vbVF10mPK743Iuv2ORLLgF/O/XkaYjYDGw5SHMof8CgiT/nfdJdlgHsOfRIg=="
},
"@trysound/sax": {
"version": "0.2.0",
@@ -8426,6 +8279,13 @@
"dev": true,
"peer": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true,
"peer": true
},
"@types/pug": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz",
@@ -8463,58 +8323,15 @@
}
},
"@typescript-eslint/parser": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.0.tgz",
"integrity": "sha512-qK9TZ70eJtjojSUMrrEwA9ZDQ4N0e/AuoOIgXuNBorXYcBDk397D2r5MIe1B3cok/oCtdNC5j+lUUpVB+Dpb+w==",
"version": "5.57.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.0.tgz",
"integrity": "sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "5.59.0",
"@typescript-eslint/types": "5.59.0",
"@typescript-eslint/typescript-estree": "5.59.0",
"@typescript-eslint/scope-manager": "5.57.0",
"@typescript-eslint/types": "5.57.0",
"@typescript-eslint/typescript-estree": "5.57.0",
"debug": "^4.3.4"
},
"dependencies": {
"@typescript-eslint/scope-manager": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.0.tgz",
"integrity": "sha512-tsoldKaMh7izN6BvkK6zRMINj4Z2d6gGhO2UsI8zGZY3XhLq1DndP3Ycjhi1JwdwPRwtLMW4EFPgpuKhbCGOvQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.59.0",
"@typescript-eslint/visitor-keys": "5.59.0"
}
},
"@typescript-eslint/types": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.0.tgz",
"integrity": "sha512-yR2h1NotF23xFFYKHZs17QJnB51J/s+ud4PYU4MqdZbzeNxpgUr05+dNeCN/bb6raslHvGdd6BFCkVhpPk/ZeA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.0.tgz",
"integrity": "sha512-sUNnktjmI8DyGzPdZ8dRwW741zopGxltGs/SAPgGL/AAgDpiLsCFLcMNSpbfXfmnNeHmK9h3wGmCkGRGAoUZAg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.59.0",
"@typescript-eslint/visitor-keys": "5.59.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"semver": "^7.3.7",
"tsutils": "^3.21.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "5.59.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.0.tgz",
"integrity": "sha512-qZ3iXxQhanchCeaExlKPV3gDQFxMUmU35xfd5eCXB6+kUw1TUAbIy2n7QIrwz9s98DQLzNWyHp61fY0da4ZcbA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "5.59.0",
"eslint-visitor-keys": "^3.3.0"
}
}
}
},
"@typescript-eslint/scope-manager": {
@@ -9004,16 +8821,17 @@
"dev": true
},
"cosmiconfig": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz",
"integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dev": true,
"peer": true,
"requires": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"parse-json": "^5.0.0",
"path-type": "^4.0.0"
"path-type": "^4.0.0",
"yaml": "^1.10.0"
}
},
"cross-spawn": {
@@ -10038,9 +9856,9 @@
}
},
"html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
"integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz",
"integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==",
"dev": true,
"peer": true
},
@@ -10302,9 +10120,9 @@
"dev": true
},
"known-css-properties": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.27.0.tgz",
"integrity": "sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==",
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz",
"integrity": "sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==",
"dev": true,
"peer": true
},
@@ -10607,9 +10425,9 @@
"integrity": "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg=="
},
"monaco-editor-workers": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.37.0.tgz",
"integrity": "sha512-mawGzbN6PNumIME4EUXLokDHslL0Rn7gcRxLZdR+TvTpyihhQaSQFgAwZTCvNIIvNjeQH5FALzX3WFPnaUhwIA==",
"version": "0.36.0",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.36.0.tgz",
"integrity": "sha512-byrccv+VN5MC9aAjLd4wGis1cPVYtaxvBRjlqupiv8BrNehuPqRlc+T3j1A6Ow62j5AAHxnzHtWkIwPvDDmYIg==",
"dev": true
},
"monaco-languageclient": {
@@ -11926,21 +11744,17 @@
}
},
"stylelint": {
"version": "15.5.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.5.0.tgz",
"integrity": "sha512-jyMO3R1QtE5mUS4v40+Gg+sIQBqe7CF1xPslxycDzNVkIBCUD4O+5F1vLPq16VmunUTv4qG9o2rUKLnU5KkVeQ==",
"version": "14.16.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz",
"integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==",
"dev": true,
"peer": true,
"requires": {
"@csstools/css-parser-algorithms": "^2.1.0",
"@csstools/css-tokenizer": "^2.1.0",
"@csstools/media-query-list-parser": "^2.0.2",
"@csstools/selector-specificity": "^2.2.0",
"@csstools/selector-specificity": "^2.0.2",
"balanced-match": "^2.0.0",
"colord": "^2.9.3",
"cosmiconfig": "^8.1.3",
"cosmiconfig": "^7.1.0",
"css-functions-list": "^3.1.0",
"css-tree": "^2.3.1",
"debug": "^4.3.4",
"fast-glob": "^3.2.12",
"fastest-levenshtein": "^1.0.16",
@@ -11948,18 +11762,18 @@
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
"html-tags": "^3.3.1",
"ignore": "^5.2.4",
"html-tags": "^3.2.0",
"ignore": "^5.2.1",
"import-lazy": "^4.0.0",
"imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0",
"known-css-properties": "^0.27.0",
"known-css-properties": "^0.26.0",
"mathml-tag-names": "^2.1.3",
"meow": "^9.0.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.21",
"postcss": "^8.4.19",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0",
@@ -11969,11 +11783,11 @@
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"style-search": "^0.1.0",
"supports-hyperlinks": "^3.0.0",
"supports-hyperlinks": "^2.3.0",
"svg-tags": "^1.0.0",
"table": "^6.8.1",
"v8-compile-cache": "^2.3.0",
"write-file-atomic": "^5.0.0"
"write-file-atomic": "^4.0.2"
},
"dependencies": {
"balanced-match": {
@@ -12004,9 +11818,9 @@
}
},
"stylelint-config-recommended": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-12.0.0.tgz",
"integrity": "sha512-x6x8QNARrGO2sG6iURkzqL+Dp+4bJorPMMRNPScdvaUK8PsynriOcMW7AFDKqkWAS5wbue/u8fUT/4ynzcmqdQ==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz",
"integrity": "sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==",
"dev": true,
"requires": {}
},
@@ -12056,9 +11870,9 @@
}
},
"supports-hyperlinks": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz",
"integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
"integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
"dev": true,
"peer": true,
"requires": {
@@ -12108,9 +11922,9 @@
"requires": {}
},
"svelte-check": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.2.0.tgz",
"integrity": "sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.1.4.tgz",
"integrity": "sha512-25Lb46ZS4IK/XpBMe4IBMrtYf23V8alqBX+szXoccb7uM0D2Wqq5rMRzYBONZnFVuU1bQG3R50lyIT5eRewv2g==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -12119,8 +11933,8 @@
"import-fresh": "^3.2.1",
"picocolors": "^1.0.0",
"sade": "^1.7.4",
"svelte-preprocess": "^5.0.3",
"typescript": "^5.0.3"
"svelte-preprocess": "^5.0.0",
"typescript": "^4.9.4"
}
},
"svelte-floating-ui": {
@@ -12512,9 +12326,9 @@
"dev": true
},
"typescript": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true
},
"uglify-js": {
@@ -12744,9 +12558,9 @@
"dev": true
},
"write-file-atomic": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.0.tgz",
"integrity": "sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"peer": true,
"requires": {

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.88.0",
"version": "1.87.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -9,7 +9,7 @@
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .",
"package": "svelte-package",
"package": "svelte-package && cd package && rm README.md && rm README_DEV.md && sed -i -e 's/windmill/windmill-components/g' package.json",
"generate-backend-client": "openapi --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions && sed -i '213 i \\ request.referrerPolicy = \"no-referrer\"\n' src/lib/gen/core/request.ts",
"pretest": "tsc --incremental -p tests/tsconfig.json",
"test": "playwright test --config=tests-out/playwright.config.js"
@@ -17,9 +17,9 @@
"devDependencies": {
"@playwright/test": "^1.31.1",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.15.6",
"@sveltejs/package": "^2.0.2",
"@sveltejs/adapter-static": "^2.0.1",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
@@ -28,7 +28,7 @@
"@types/node": "^18.11.18",
"@types/vscode": "~1.77.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.59.0",
"@typescript-eslint/parser": "^5.48.0",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^6.0.0",
@@ -36,7 +36,7 @@
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"monaco-editor-workers": "0.37.0",
"monaco-editor-workers": "0.36.0",
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
@@ -47,11 +47,11 @@
"prettier-plugin-svelte": "^2.9.0",
"simple-svelte-autocomplete": "^2.5.1",
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
"svelte-awesome": "^3.2.0",
"svelte-awesome-color-picker": "^2.4.1",
"svelte-check": "^3.2.0",
"svelte-check": "^3.0.2",
"svelte-highlight": "^7.2.1",
"svelte-multiselect": "^8.6.0",
"svelte-overlay": "^1.4.1",
@@ -62,7 +62,7 @@
"svelte2tsx": "^0.6.11",
"tailwindcss": "^3.2.2",
"tslib": "^2.4.1",
"typescript": "^5.0.4",
"typescript": "^4.9.3",
"vite": "^4.0.4",
"yootils": "^0.3.1"
},
@@ -73,7 +73,7 @@
"@leeoniya/ufuzzy": "^1.0.6",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.8.5",
"@tanstack/svelte-table": "^8.7.6",
"ag-grid-svelte": "^0.1.4",
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^3.0.0",
@@ -96,17 +96,6 @@
"vscode-ws-jsonrpc": "3.0.0"
},
"peerDependencies": {
"@sveltejs/kit": "^1.15.6",
"svelte": "^3.55.1"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
},
"files": [
"dist"
],
"license": "Apache-2.0"
}
}

View File

@@ -1,4 +0,0 @@
npm run package
sed -i -e 's/windmill/windmill-components/g' package.json
npm publish
sed -i -e 's/windmill-components/windmill/g' package.json

View File

@@ -2,12 +2,7 @@
const apiTokenApps: Record<string, { img?: string; instructions: string[]; key?: string }> = {
airtable: {
img: '/airtable_connect.png',
instructions: [
'Click to <a href="https://airtable.com/create/tokens" target="_blank" rel=”noopener noreferrer”>https://airtable.com/create/tokens</a>',
'Click on "Create new token"',
'Set a name, specify the scopes or the access level and click on "Create token"',
'Copy the token'
]
instructions: ['Click on the top-right avatar', 'Click on Account', 'Find "Api"']
},
discord_webhook: {
img: '/discord_webhook.png',
@@ -461,7 +456,7 @@
>
{/if}
{/if}
<div slot="actions" class="flex gap-1">
<div slot="actions">
{#if step > 1 && !no_back}
<Button variant="border" on:click={back}>Back</Button>
{/if}

View File

@@ -311,23 +311,21 @@
{/if}
{/key}
</div>
<div class="flex mt-2">
<Button
variant="border"
color="dark"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
</div>
<Button
variant="border"
color="dark"
size="xs"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
</div>
{:else if inputCat == 'resource-object'}
<ObjectResourceInput {format} bind:value />

View File

@@ -11,7 +11,6 @@
export let result: any
export let requireHtmlApproval = false
export let filename: string | undefined = undefined
export let disableExpand = false
let resultKind:
| 'json'
@@ -102,12 +101,10 @@
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
class="mb-2 w-full text-sm text-gray-700 relative"
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
{#if !disableExpand}
<div class="text-gray-500 text-xs absolute top-5 right-0">
<button on:click={jsonViewer.openDrawer}>Expand</button>
</div>
{/if}
</div>{/if}{#if !forceJson && resultKind == 'table-col'}<div
<div class="text-gray-500 text-xs absolute top-5 right-0">
<button on:click={jsonViewer.openDrawer}>Expand</button>
</div></div
>{/if}{#if !forceJson && resultKind == 'table-col'}<div
class="grid grid-flow-col-dense border border-gray-200 rounded-md"
>
{#each Object.keys(result) as col}
@@ -248,21 +245,19 @@
{/if}
</div>
{#if !disableExpand}
<Portal>
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
<Button
on:click={() => copyToClipboard(JSON.stringify(result, null, 4))}
color="light"
size="xs"
>
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
</Button>
</svelte:fragment>
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
</DrawerContent>
</Drawer>
</Portal>
{/if}
<Portal>
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
<Button
on:click={() => copyToClipboard(JSON.stringify(result, null, 4))}
color="light"
size="xs"
>
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
</Button>
</svelte:fragment>
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
</DrawerContent>
</Drawer>
</Portal>

View File

@@ -11,21 +11,13 @@
type Side = 'top' | 'bottom'
type Placement = `${Side}-${Alignment}`
export let dropdownItems: DropdownItem[] | (() => DropdownItem[]) = []
export let dropdownItems: DropdownItem[]
export let name: string | undefined = undefined
export let placement: Placement = 'bottom-start'
export let btnClasses = ''
$: buttonClass = twMerge('!border-0 bg-transparent !p-[6px]', btnClasses)
const dispatch = createEventDispatcher()
function computeDropdowns(): DropdownItem[] {
if (typeof dropdownItems === 'function') {
return dropdownItems()
} else {
return dropdownItems
}
}
</script>
<Menu {placement} let:close>
@@ -45,7 +37,7 @@
{/if}
</Button>
{#if dropdownItems}
{#each computeDropdowns() as item, i}
{#each dropdownItems as item, i}
{#if item.action}
<button
on:click|preventDefault|stopPropagation={(event) => {
@@ -99,8 +91,8 @@
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class:bg-gray-200={item.disabled}
class="block font-semibold text-left px-4 py-2 text-sm text-gray-700 cursor-auto"
class:bg-gray-50={item.disabled}
class="block text-left px-4 py-2 text-sm text-gray-700 cursor-auto"
role="menuitem"
tabindex="-1"
id="user-menu-item-{name}-{i}}"

View File

@@ -508,7 +508,7 @@
async function loadMonaco() {
const model = meditor.createModel(code, lang, mUri.parse(uri))
model.updateOptions(lang == 'python' ? { tabSize: 4, insertSpaces: true } : updateOptions)
model.updateOptions(updateOptions)
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets),
overflowWidgetsDomNode: widgets,
@@ -569,7 +569,7 @@
callback: (editor: meditor.IStandaloneCodeEditor) => void,
keybindings: number[] = []
) {
editor?.addAction({
editor.addAction({
id,
label,
keybindings,

View File

@@ -26,11 +26,11 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { Badge, Drawer } from './common'
import WorkspaceScriptPicker from './flows/pickers/WorkspaceScriptPicker.svelte'
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'
import PickHubItems from './flows/pickers/PickHubItems.svelte'
export let lang: 'python3' | 'deno' | 'go' | 'bash'
export let editor: Editor | undefined
@@ -86,9 +86,9 @@
<Drawer bind:this={scriptPicker} size="900px">
<DrawerContent title="Code" on:close={scriptPicker.closeDrawer}>
{#if pick_existing == 'hub'}
<PickHubItems bind:filter {kind} on:pickScript={onScriptPick} selectedKind="script">
<PickHubScript bind:filter {kind} on:pick={onScriptPick}>
<ToggleHubWorkspace bind:selected={pick_existing} />
</PickHubItems>
</PickHubScript>
{:else}
<WorkspaceScriptPicker bind:filter {kind} on:pick={onScriptPick}>
<ToggleHubWorkspace bind:selected={pick_existing} />

View File

@@ -118,10 +118,10 @@
}
loadingSave = false
if (leave) {
goto(`/flows/get/${$flowStore.path}?workspace=${$workspaceStore}`)
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
} else if (initialPath !== $flowStore.path) {
initialPath = $flowStore.path
goto(`/flows/edit/${$flowStore.path}?workspace=${$workspaceStore}`)
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
}
} catch (err) {
sendUserToast(`The flow could not be saved: ${err.body}`, true)
@@ -362,7 +362,6 @@
<FlowPreviewButtons />
<div class="center-center">
<Button
title="Ctrl/Cmd + S"
loading={loadingSave}
size="xs"
startIcon={{ icon: faSave }}

View File

@@ -158,8 +158,8 @@
timeout && clearTimeout(timeout)
})
function loadOwner(path: string) {
is_owner = isOwner(path, $userStore!, workspaceId ?? $workspaceStore!)
async function loadOwner(path: string) {
is_owner = await isOwner(path, $userStore!, workspaceId ?? $workspaceStore!)
}
let selected: 'graph' | 'sequence' = 'graph'

View File

@@ -4,10 +4,9 @@
import TableCustom from './TableCustom.svelte'
export let args: any
export let tableClass = ''
</script>
<TableCustom class="py-2 {tableClass}">
<TableCustom class="py-2">
<tr slot="header-row">
<th>Argument</th>
<th>Value</th>

View File

@@ -220,23 +220,21 @@
</div>
{/each}
</div>
<div class="flex">
<Button
variant="border"
color="blue"
size="sm"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
</div>
<Button
variant="border"
color="blue"
size="sm"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
<span class="ml-2">
{(value ?? []).length} item{(value ?? []).length > 1 ? 's' : ''}
</span>

View File

@@ -6,7 +6,6 @@
export let isLoading: boolean
export let duration: number | undefined = undefined
export let mem: number | undefined = undefined
export let wrapperClass = ''
let scroll = true
let div: HTMLElement | null = null
@@ -34,7 +33,7 @@
</DrawerContent>
</Drawer>
<div class="relative w-full h-full {wrapperClass}">
<div class="relative w-full h-full">
<div bind:this={div} class="w-full h-full overflow-auto bg-gray-50 relative">
<div
class="sticky top-0 right-0 w-full flex flex-row-reverse justify-between text-gray-500 text-sm bg-gray-50/20"

View File

@@ -27,12 +27,12 @@
kind = kind_l
initialPath = initialPath_l
summary = summary_l
loadOwner()
await loadOwner()
drawer.openDrawer()
}
function loadOwner() {
own = isOwner(initialPath, $userStore!, $workspaceStore!)
async function loadOwner() {
own = await isOwner(initialPath, $userStore!, $workspaceStore!)
}
async function updatePath() {
@@ -85,12 +85,14 @@
<Drawer bind:this={drawer}>
<DrawerContent title="Move/Rename {initialPath}" on:close={drawer.closeDrawer}>
<h1 class="mb-2">Move/Rename {initialPath}</h1>
{#if !own}
<Alert type="warning" title="Not owner"
>Since you do not own this item, you cannot move this item (you can however fork it)</Alert
>
{/if}
<h2 class="border-b pb-1 mt-2 mb-4">Summary</h2>
<h2 class="border-b pb-1 mt-8 mb-4">Summary</h2>
<input
type="text"
bind:value={summary}
@@ -98,10 +100,9 @@
disabled={!own}
/>
<h2 class="border-b pb-1 mt-10 mb-4">Path</h2>
<h2 class="border-b pb-1 mt-8 mb-4">Path</h2>
<div class="flex flex-col mb-2 gap-6">
<Path disabled={!own} {kind} {initialPath} bind:path />
<div class="mt-4" />
<Button disabled={!own} on:click={updatePath}>Move/Rename</Button>
<div />
</div>

View File

@@ -337,7 +337,6 @@
btnClasses="!p-1.5"
variant="border"
size="xs"
{disabled}
on:click={newFolder.openDrawer}
>
<Icon scale={0.8} data={faPlus} /></Button

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ResourceService, type Resource } from '$lib/gen'
import { canWrite, emptyString, isOwner, sendUserToast } from '$lib/utils'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { Alert, Button, Drawer, Skeleton } from './common'
import Path from './Path.svelte'
@@ -141,7 +141,7 @@
</div>
{/if}
<Path
disabled={initialPath != '' && !isOwner(initialPath, $userStore, $workspaceStore)}
disabled={!can_write}
bind:path
{initialPath}
namePlaceholder="resource"

View File

@@ -18,7 +18,6 @@
import SharedBadge from './SharedBadge.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
import CollapseLink from './CollapseLink.svelte'
export let runnable:
| {
@@ -137,67 +136,62 @@
<div class="text-xs text-gray-600">No schema</div>
{/if}
{#if schedulable}
<div class="mt-10" />
<CollapseLink text="Advanced">
<div class="flex flex-col gap-4 mt-2 border p-2">
<div class="flex flex-col gap-2">
<div class="flex">
<Button
color="light"
size="sm"
endIcon={{ icon: viewOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewOptions = !viewOptions)}
>
Schedule to run later
</Button>
</div>
{#if viewOptions}
<div transition:slide|local class="mt-6">
<div class="border rounded-md p-3 pt-4">
<div class="flex flex-row items-end">
<div class="w-max md:w-2/3 mt-2 mb-1">
<label for="run-time" />
<input
class="inline-block"
type="datetime-local"
id="run-time"
name="run-scheduled-time"
bind:value={scheduledForStr}
min={getToday().toISOString().slice(0, 16)}
/>
</div>
<Button
variant="border"
color="blue"
size="sm"
btnClasses="mx-2 mb-1"
on:click={() => {
scheduledForStr = undefined
}}
>
Clear
</Button>
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6 mb-6">
<div class="flex flex-col">
<div>
<Button
color="light"
size="sm"
endIcon={{ icon: viewOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewOptions = !viewOptions)}
>
Schedule to run later
</Button>
</div>
{#if viewOptions}
<div transition:slide|local class="mt-6">
<div class="border rounded-md p-3 pt-4">
<div class="flex flex-row items-end">
<div class="w-max md:w-2/3 mt-2 mb-1">
<label for="run-time" />
<input
class="inline-block"
type="datetime-local"
id="run-time"
name="run-scheduled-time"
bind:value={scheduledForStr}
min={getToday().toISOString().slice(0, 16)}
/>
</div>
<Button
variant="border"
color="blue"
size="sm"
btnClasses="mx-2 mb-1"
on:click={() => {
scheduledForStr = undefined
}}
>
Clear
</Button>
</div>
</div>
{/if}
</div>
{#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2}
<div class="flex items-center gap-1">
<Toggle
options={{
right: `make run invisible to others`
}}
bind:checked={invisible_to_owner}
/>
<Tooltip
>By default, runs are visible to the owner(s) of the script or flow being triggered</Tooltip
>
</div>
{/if}
</div>
</CollapseLink>
<div class="flex gap-2 items-start flex-wrap justify-between mt-2 md:mt-6 mb-6">
{#if runnable?.path?.startsWith(`u/${$userStore?.username}`) != true && (runnable?.path?.split('/')?.length ?? 0) > 2}
<div class="flex items-center gap-1">
<Toggle
options={{
right: `make run invisible to others`
}}
bind:checked={invisible_to_owner}
/>
<Tooltip
>By default, runs are visible to the owner(s) of the script or flow being triggered</Tooltip
>
</div>
{/if}
<div class="flex-row-reverse flex grow">
<Button
{loading}
@@ -220,24 +214,20 @@
{/if}
{#if viewCliRun}
<div>
<div class="my-20" />
<div class="flex">
<Button
color="light"
size="xs"
endIcon={{ icon: viewCliOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewCliOptions = !viewCliOptions)}
>
Run it from the CLI
</Button>
<div class="my-10" />
<Button
color="light"
size="xs"
endIcon={{ icon: viewCliOptions ? faChevronUp : faChevronDown }}
on:click={() => (viewCliOptions = !viewCliOptions)}
>
Run it from the CLI
</Button>
{#if viewCliOptions}
<div transition:slide|local class="mt-2 px-4 pt-2">
<InlineCodeCopy content={cliCommand} />
<CliHelpBox />
</div>
{#if viewCliOptions}
<div transition:slide|local class="mt-2 px-4 pt-2">
<InlineCodeCopy content={cliCommand} />
<CliHelpBox />
</div>
{/if}
</div>
{/if}
{/if}
</div>

View File

@@ -142,7 +142,7 @@
<div class="min-w-[300px] h-full">
<Splitpanes horizontal={true}>
<Pane>
<div class="w-full flex flex-col gap-4 p-2">
<div class="w-full flex flex-col gap-4 p-4">
<div class="w-full flex justify-between items-center gap-4 flex-wrap">
<span class="text-sm font-extrabold flex-shrink-0"
>Saved Inputs <Tooltip
@@ -256,7 +256,7 @@
</Pane>
<Pane>
<div class="w-full flex flex-col gap-4 p-2">
<div class="w-full flex flex-col gap-4 p-4">
<span class="text-sm font-extrabold">Previous Inputs</span>
<div class="w-full flex flex-col gap-1 p-0 h-full overflow-y-auto">
@@ -291,37 +291,35 @@
</div>
</Pane>
<Pane>
<div class="h-full overflow-hidden min-h-0 flex flex-col justify-between">
<div class="w-full flex flex-col min-h-0 gap-2 px-2 py-2 grow">
<div class="text-sm font-extrabold">Preview</div>
<Pane class="flex flex-col justify-between">
<div class="w-full flex flex-col gap-4 p-4 h-full">
<span class="text-sm font-extrabold">Preview</span>
<div class="w-full min-h-0 grow overflow-auto">
{#if Object.keys(selectedInput?.args || {}).length > 0}
<div class="border overflow-auto h-full p-2">
<ObjectViewer json={selectedInput?.args} />
</div>
{:else}
<div class="text-center text-gray-500">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
<div class="w-full h-full overflow-auto">
{#if Object.keys(selectedInput?.args || {}).length > 0}
<div class="border h-full p-2">
<ObjectViewer json={selectedInput?.args} />
</div>
{:else}
<div class="text-center text-gray-500">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
</div>
<div class="w-full flex flex-col px-2 pb-2">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={() => selectArgs(selectedInput?.args)}
disabled={Object.keys(selectedInput?.args || {}).length === 0}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use Input
</Button>
</div>
<div class="w-full flex flex-col p-4">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={() => selectArgs(selectedInput?.args)}
disabled={Object.keys(selectedInput?.args || {}).length === 0}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use Input
</Button>
</div>
</Pane>
</Splitpanes>

View File

@@ -85,7 +85,6 @@
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
editing = false
oldArgName = undefined
schemaModal.closeDrawer()
}
schema = schema
@@ -265,7 +264,7 @@
{#if !emptyString(error)}<span class="text-red-400">{error}</span>{:else}<div
class="py-6"
/>{/if}
<div class="border rounded p-2">
<div class="border rounded p-2">
<SimpleEditor
fixedOverflowWidgets={false}
on:change={() => {

View File

@@ -30,7 +30,6 @@
default: schema.default,
contentEncoding: schema.contentEncoding,
format: schema.format,
enum_: schema.enum,
required
}
}
@@ -91,7 +90,6 @@
property.selectedType = DEFAULT_PROPERTY.selectedType
property.format = undefined
resource_type = undefined
drawer.closeDrawer()
}
@@ -211,6 +209,7 @@
</div>
{/if}
</div>
<svelte:fragment slot="actions">
<Button
color="blue"

View File

@@ -83,17 +83,6 @@
scriptEditor?.inferSchema(script.content, language)
}
async function leaveF(leave: boolean, newHash: string) {
if (leave) {
history.replaceState(history.state, '', `/scripts/edit/${newHash}`)
goto(`/scripts/get/${newHash}?workspace=${$workspaceStore}`)
} else {
await goto(`/scripts/edit/${newHash}`)
script.hash = newHash
topHash = undefined
}
}
async function editScript(leave: boolean): Promise<void> {
loadingSave = true
try {
@@ -123,37 +112,34 @@
kind: script.kind
}
})
leaveF(leave, newHash)
if (leave) {
history.replaceState(history.state, '', `/scripts/edit/${newHash}`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
} else {
await goto(`/scripts/edit/${newHash}`)
script.hash = newHash
topHash = undefined
}
} catch (error) {
sendUserToast(`Error while saving the script: ${error.body || error.message}`, true)
}
loadingSave = false
}
function computeDropdownItems() {
let dropdownItems: { label: string; onClick: () => void }[] = []
if ($dirtyStore) {
dropdownItems.push({
label: 'Save and see details',
onClick: () => editScript(true)
})
const dropdownItems: Array<{ label: string; onClick: () => void }> = [
{
label: 'Save and leave',
onClick: () => editScript(true)
}
]
if (initialPath != '') {
dropdownItems.push({
label: 'See details',
onClick: () => leaveF(true, script.hash)
label: 'Fork',
onClick: () => {
window.open(`/scripts/add?template=${initialPath}`)
}
})
if (initialPath != '') {
dropdownItems.push({
label: 'Fork',
onClick: () => {
window.open(`/scripts/add?template=${initialPath}`)
}
})
}
return dropdownItems
}
</script>
@@ -300,13 +286,12 @@
Customise
</Button>
<Button
disabled={!$dirtyStore}
color="dark"
loading={loadingSave}
size="sm"
startIcon={{ icon: faSave }}
on:click={() => editScript(false)}
dropdownItems={computeDropdownItems}
{dropdownItems}
>
Save
</Button>

View File

@@ -38,14 +38,13 @@
loadAcls()
loadGroups()
loadUsernames()
loadOwner()
await loadOwner()
drawer.openDrawer()
}
async function loadOwner() {
own = isOwner(path, $userStore!, $workspaceStore!)
own = await isOwner(path, $userStore!, $workspaceStore!)
}
async function loadAcls() {
acls = Object.entries(
await GranularAclService.getGranularAcls({ workspace: $workspaceStore!, path, kind })

View File

@@ -11,43 +11,39 @@
let reason = ''
$: {
if ($userStore?.is_admin || $userStore?.is_super_admin) {
kind = undefined
} else {
let username = $userStore?.username ?? ''
let pgroups = $userStore?.pgroups ?? []
let pusername = `u/${username}`
let extraPermsKeys = Object.keys(extraPerms)
let username = $userStore?.username ?? ''
let pgroups = $userStore?.pgroups ?? []
let pusername = `u/${username}`
let extraPermsKeys = Object.keys(extraPerms)
if (pusername in extraPermsKeys) {
if (extraPerms[pusername]) {
kind = 'write'
} else {
kind = 'read'
}
reason = 'This item was shared to you personally'
if (pusername in extraPermsKeys) {
if (extraPerms[pusername]) {
kind = 'write'
} else {
let writeGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms[x])
if (writeGroup) {
kind = 'write'
reason = `This item was write shared to the group ${writeGroup} which you are a member of`
kind = 'read'
}
reason = 'This item was shared to you personally'
} else {
let writeGroup = pgroups.find((x) => extraPermsKeys.includes(x) && extraPerms[x])
if (writeGroup) {
kind = 'write'
reason = `This item was write shared to the group ${writeGroup} which you are a member of`
} else {
let readGroup = pgroups.find((x) => extraPermsKeys.includes(x))
if (readGroup) {
kind = 'read'
reason = `This item was read-only shared to the group ${readGroup} which you are a member of`
} else {
let readGroup = pgroups.find((x) => extraPermsKeys.includes(x))
if (readGroup) {
kind = 'read'
reason = `This item was read-only shared to the group ${readGroup} which you are a member of`
} else {
kind = undefined
}
kind = undefined
}
}
if (kind == 'read' && canWrite) {
kind = undefined
}
if (kind == undefined && !canWrite) {
kind = 'read'
reason = ''
}
}
if (kind == 'read' && canWrite) {
kind = undefined
}
if (kind == undefined && !canWrite) {
kind = 'read'
reason = ''
}
}
</script>

View File

@@ -10,6 +10,7 @@
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' = computeKind()
let patternStr: string = pattern ?? ''
let resource: string | undefined
const FORMATS = [
@@ -77,8 +78,8 @@
<div class="my-2" />
{#if kind == 'pattern'}
<label for="input" class="mb-2 text-gray-700 text-xs">
Pattern (Regex)
<label for="input" class="mb-2 text-gray-700 text-xs"
>Pattern (Regex)
<div class="flex flex-row">
<input
id="input"
@@ -93,10 +94,8 @@
btnClasses="mx-2 mb-1"
on:click={() => {
patternStr = ''
}}
}}>clear</Button
>
clear
</Button>
</div>
</label>
{:else if kind == 'enum'}

View File

@@ -146,7 +146,7 @@
if (previewJobUpdates.new_logs) {
job.logs = (job?.logs ?? '').concat(previewJobUpdates.new_logs)
}
if (previewJobUpdates.mem_peak && job) {
if (previewJobUpdates.mem_peak) {
job.mem_peak = previewJobUpdates.mem_peak
}
if ((previewJobUpdates.running ?? false) || (previewJobUpdates.completed ?? false)) {
@@ -198,19 +198,3 @@
currentId = undefined
})
</script>
<slot
{job}
{isLoading}
{workspaceOverride}
{notfound}
{abstractRun}
{runScriptByPath}
{runFlowByPath}
{runPreview}
{cancelJob}
{clearCurrentJob}
{watchJob}
{loadTestJob}
{syncer}
/>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { canWrite, isOwner, sendUserToast } from '$lib/utils'
import { canWrite, sendUserToast } from '$lib/utils'
import { VariableService } from '$lib/gen'
import Path from './Path.svelte'
import { createEventDispatcher } from 'svelte'
@@ -145,7 +145,7 @@
{/if}
<span class="font-semibold text-gray-700">Path</span>
<Path
disabled={initialPath != '' && !isOwner(initialPath, $userStore, $workspaceStore)}
disabled={!can_write}
bind:error={pathError}
bind:path
{initialPath}

View File

@@ -142,9 +142,7 @@
{#if resolvedConfig.beforeIcon && beforeIconComponent}
<svelte:component this={beforeIconComponent} size={14} />
{/if}
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
<div>{resolvedConfig.label}</div>
{/if}
<div>{resolvedConfig.label}</div>
{#if resolvedConfig.afterIcon && afterIconComponent}
<svelte:component this={afterIconComponent} size={14} />
{/if}

View File

@@ -79,18 +79,16 @@
function getMarkerArray(): Marker[] | undefined {
let array: Marker[] | undefined = undefined
try {
if (typeof markers === 'string') {
const json = JSON.parse(markers)
array = Array.isArray(json) ? json : [json]
} else {
array = markers
if (typeof markers === 'string') {
try {
array = JSON.parse(markers)
} catch (e) {
return undefined
}
return array?.filter((m) => !isNaN(+m.lat) && !isNaN(+m.lon))
} catch (error) {
console.log(error)
return undefined
} else {
array = markers
}
return array?.filter((m) => !isNaN(+m.lat) && !isNaN(+m.lon))
}
function createMarkerLayers() {

View File

@@ -11,7 +11,7 @@
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, CancelablePromise, InlineScript } from '../../types'
import type { AppViewerContext, InlineScript } from '../../types'
import { computeGlobalContext, eval_like } from './eval'
import InputValue from './InputValue.svelte'
import RefreshButton from './RefreshButton.svelte'
@@ -58,29 +58,12 @@
const dispatch = createEventDispatcher()
let donePromise: (() => void) | undefined = undefined
const cancellableRun: (inlineScript?: InlineScript) => CancelablePromise<void> = (
inlineScript?: InlineScript
) => {
let rejectCb: (err: Error) => void
let p: Partial<CancelablePromise<void>> = new Promise<void>((resolve, reject) => {
rejectCb = reject
donePromise = resolve
executeComponent(true, inlineScript).catch(reject)
})
p.cancel = () => {
testJobLoader?.cancelJob()
loading = false
rejectCb(new Error('Canceled'))
}
return p as CancelablePromise<void>
}
$runnableComponents[id] = {
autoRefresh: autoRefresh && recomputableByRefreshButton,
refreshOnStart,
cb: cancellableRun
cb: async (inlineScript?: InlineScript) => {
await executeComponent(true, inlineScript)
}
}
if (!$initialized.initializedComponents.includes(id)) {
@@ -389,7 +372,6 @@
if (startedAt > lastStartedAt) {
lastStartedAt = startedAt
setResult(e.detail.result)
donePromise?.()
}
}
loading = false

View File

@@ -396,7 +396,7 @@
<Skeleton layout={[[40]]} />
{/if}
{:else}
<div class="flex flex-col h-full w-full gap-4 p-2 mb-4">
<div class="flex flex-col h-full w-full gap-4 mb-4">
{#if job?.['running']}
<div class="flex flex-row-reverse w-full">
<Button
@@ -404,8 +404,6 @@
variant="border"
on:click={() => testJobLoader?.cancelJob()}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
</div>
@@ -635,12 +633,7 @@
</span>
<span class="hidden md:inline">
<Button
on:click={() => {
if (selectedJobId == undefined && $jobs.length > 0) {
selectedJobId = $jobs[0].job
}
jobsDrawerOpen = true
}}
on:click={() => (jobsDrawerOpen = true)}
color={hasErrors ? 'red' : 'light'}
size="xs"
variant="border"

View File

@@ -11,7 +11,7 @@
import { sendUserToast } from '$lib/utils'
import { gridColumns } from '../../gridUtils'
const { app, selectedComponent, focusedGrid, componentControl } =
const { app, selectedComponent, worldStore, focusedGrid, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
const { history, movingcomponents } = getContext<AppEditorContext>('AppEditorContext')
@@ -209,6 +209,7 @@
$selectedComponent = nitems.map((x) => x)
}
$worldStore = $worldStore
$app = $app
}

View File

@@ -1642,7 +1642,7 @@ Hello \${ctx.username}
fieldType: 'text',
fileUpload: {
accept: 'application/pdf',
convertTo: 'base64'
convertTo: 'buffer'
}
},
zoom: {

View File

@@ -15,7 +15,8 @@
import { push } from '$lib/history'
import { flip } from 'svelte/animate'
const { app, selectedComponent, focusedGrid } = getContext<AppViewerContext>('AppViewerContext')
const { app, selectedComponent, focusedGrid, worldStore } =
getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
@@ -32,6 +33,7 @@
$selectedComponent = [id]
$app = $app
$worldStore = $worldStore
}
let search = ''

View File

@@ -135,7 +135,9 @@
runnable?.type === 'runnableByName' &&
runnable?.inlineScript?.refreshOn?.find((x) => x.id === from)
) {
console.log('processss')
runnable.inlineScript.refreshOn = runnable.inlineScript.refreshOn.map((x) => {
console.log('renaming', x)
if (x.id === from) {
return {
id: to,

View File

@@ -2,6 +2,7 @@
import type { Schema } from '$lib/common'
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Script, type Preview } from '$lib/gen'
import { inferArgs } from '$lib/infer'
@@ -15,7 +16,6 @@
import { defaultCode } from '../component'
import InlineScriptList from '../settingsPanel/mainInput/InlineScriptList.svelte'
import WorkspaceScriptList from '../settingsPanel/mainInput/WorkspaceScriptList.svelte'
import PickHubItems from '$lib/components/flows/pickers/PickHubItems.svelte'
export let name: string
export let componentType: string | undefined = undefined
@@ -130,11 +130,7 @@
{:else if tab == 'workspacescripts'}
<WorkspaceScriptList on:pick={(e) => pickScript(e.detail)} />
{:else if tab == 'hubscripts'}
<PickHubItems
selectedKind="script"
bind:filter
on:pickScript={(e) => pickHubScript(e.detail.path)}
/>
<PickHubScript bind:filter on:pick={(e) => pickHubScript(e.detail.path)} />
{/if}
</div>
</div>

View File

@@ -2,8 +2,8 @@
import Button from '$lib/components/common/button/Button.svelte'
import type { Preview } from '$lib/gen'
import { createEventDispatcher, getContext, onMount } from 'svelte'
import type { AppViewerContext, CancelablePromise, InlineScript } from '../../types'
import { CornerDownLeft, Loader2, Maximize2, Trash2 } from 'lucide-svelte'
import type { AppViewerContext, InlineScript } from '../../types'
import { CornerDownLeft, Maximize2, Trash2 } from 'lucide-svelte'
import InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
@@ -88,7 +88,21 @@
? buildExtraLib($worldStore?.outputsById ?? {}, id, false, $state, true)
: undefined
let cancelable: CancelablePromise<void> | undefined = undefined
let refreshOn: string = inlineScript?.refreshOn?.map((x) => `${x.id}.${x.key}`).join(' ') ?? ''
$: handleRefreshOn(refreshOn)
function handleRefreshOn(refreshOn: string) {
if (refreshOn && refreshOn != '' && inlineScript) {
inlineScript.refreshOn = refreshOn
.split(' ')
.filter((x) => x.split('.').length == 2)
.map((x) => {
const [id, key] = x.split('.')
return { id, key }
})
}
}
</script>
{#if inlineScript}
@@ -168,52 +182,31 @@
</div>
</Button>
{#if $runnableComponents[id] != undefined}
{#if !runLoading}
<Button
loading={runLoading}
size="xs"
color="dark"
variant="border"
btnClasses="!px-2 !py-1 !bg-gray-700 !text-white hover:!bg-gray-900"
on:click={async () => {
runLoading = true
try {
cancelable = $runnableComponents[id]?.cb?.(
!transformer ? inlineScript : undefined
)
await cancelable
} catch {}
runLoading = false
}}
>
<div class="flex flex-row gap-1 items-center">
Run
<Button
loading={runLoading}
size="xs"
color="dark"
variant="border"
btnClasses="!px-2 !py-1 !bg-gray-700 !text-white hover:!bg-gray-900"
on:click={async () => {
runLoading = true
await $runnableComponents[id]?.cb?.(!transformer ? inlineScript : undefined)
runLoading = false
}}
>
<div class="flex flex-row gap-1 items-center">
Run
<div class="flex flex-row items-center gap-1">
<Kbd>{getModifierKey()}</Kbd>
<Kbd>
<div class="h-4 flex items-center justify-center">
<CornerDownLeft size={10} />
</div>
</Kbd>
</div>
<div class="flex flex-row items-center gap-1">
<Kbd>{getModifierKey()}</Kbd>
<Kbd>
<div class="h-4 flex items-center justify-center">
<CornerDownLeft size={10} />
</div>
</Kbd>
</div>
</Button>
{:else}
<Button
size="xs"
color="red"
variant="border"
btnClasses="!px-2 !py-1.5"
on:click={async () => {
cancelable?.cancel()
runLoading = false
}}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
{/if}
</div>
</Button>
{/if}
</div>
</div>

View File

@@ -28,7 +28,7 @@
<ScriptEditor
noSyncFromGithub
lang={inlineScript.language}
path={inlineScript.path ? inlineScript.path + '_fullscreen' : undefined}
path={inlineScript.path}
fixedOverflowWidgets={false}
bind:code={inlineScript.content}
bind:schema={inlineScript.schema}

View File

@@ -67,17 +67,13 @@
}
async function refreshFlow(x: RunnableByPath) {
const schema = (await loadSchema($workspaceStore ?? '', x.path, 'flow')) ?? emptySchema()
x.schema = schema
if (componentInput?.type == 'runnable') {
const schema = (await loadSchema($workspaceStore ?? '', x.path, 'flow')) ?? emptySchema()
if (!deepEqual(x.schema, schema)) {
x.schema = schema
const nfields = computeFields(schema, false, componentInput.fields)
componentInput.fields = nfields
componentInput = componentInput
}
componentInput.fields = computeFields(schema, false, componentInput.fields)
}
componentInput = componentInput
}
$: if (
componentInput &&
componentInput.type == 'runnable' &&
@@ -153,7 +149,7 @@
/>
{/if}
{:else if componentInput?.runnable?.type === 'runnableByPath' && componentInput?.runnable?.path}
<div class="p-2 h-full flex flex-col gap-2">
<div class="p-2 h-full flex flex-col gap-2 ">
{#if componentInput.runnable.runType == 'script' || componentInput.runnable.runType == 'hubscript'}
<div>
<Button
@@ -222,7 +218,8 @@
startIcon={{ icon: faEye }}
endIcon={{ icon: faExternalLinkAlt }}
target="_blank"
href="/flows/get/{componentInput?.['runnable']?.path}?workspace={$workspaceStore}"
href="/flows/get/{componentInput?.['runnable']
?.path}?workspace_id={$workspaceStore}"
>
Details page
</Button>

View File

@@ -13,7 +13,7 @@
import UploadInputEditor from './inputEditor/UploadInputEditor.svelte'
import { getContext } from 'svelte'
import type { AppViewerContext, RichConfiguration } from '../../types'
import type { InputType, UploadAppInput } from '../../inputType'
import type { InputType } from '../../inputType'
export let id: string
export let componentInput: RichConfiguration
@@ -28,7 +28,7 @@
export let subFieldType: InputType | undefined
export let format: string | undefined
export let selectOptions: string[] | undefined
export let fileUpload: UploadAppInput['fileUpload'] | undefined = undefined
export let fileUpload: { accept: string; convertTo: string } | undefined = undefined
export let placeholder: string | undefined
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
@@ -139,7 +139,7 @@
{:else if componentInput?.type === 'eval'}
<EvalInputEditor {hasRows} {id} bind:componentInput />
{:else if componentInput?.type === 'upload'}
<UploadInputEditor bind:componentInput {fileUpload} />
<UploadInputEditor bind:componentInput />
{:else if componentInput?.type === 'user'}
<span class="text-2xs italic text-gray-600">Field's value is set by the user</span>
{/if}

View File

@@ -30,6 +30,7 @@
/>
{:else}
{@const meta = inputSpecsConfiguration?.[k]}
<!-- {JSON.stringify(meta)} -->
<InputsSpecEditor
key={k}
bind:componentInput={inputSpecs[k]}

View File

@@ -7,7 +7,7 @@
export let componentInput: ConnectedAppInput
const { connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
const { connectingInput, app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
function applyConnection() {
if (
@@ -22,6 +22,7 @@
hoveredComponent: undefined
}
$app = $app
$worldStore = $worldStore
}
}

View File

@@ -3,18 +3,17 @@
import type { UploadAppInput } from '../../../inputType'
export let componentInput: UploadAppInput | undefined
export let fileUpload: UploadAppInput['fileUpload'] | undefined
</script>
<FileInput
accept={fileUpload?.accept}
multiple={fileUpload?.multiple}
convertTo={fileUpload?.convertTo}
accept={componentInput?.fileUpload?.accept}
multiple={componentInput?.fileUpload?.multiple}
convertTo={componentInput?.fileUpload?.convertTo}
iconSize={24}
class="text-sm py-4"
on:change={({ detail }) => {
if (componentInput) {
componentInput.value = fileUpload?.multiple ? detail : detail?.[0]
componentInput.value = componentInput?.fileUpload?.multiple ? detail : detail?.[0]
}
}}
>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { faMousePointer, faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import { Building, Globe2 } from 'lucide-svelte'
import InlineScriptList from './InlineScriptList.svelte'
import type { ResultAppInput } from '$lib/components/apps/inputType'
@@ -11,7 +12,6 @@
import type { Schema } from '$lib/common'
import { getAllScriptNames, loadSchema, schemaToInputsSpec } from '$lib/components/apps/utils'
import { emptySchema } from '$lib/utils'
import PickHubItems from '$lib/components/flows/pickers/PickHubItems.svelte'
type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts'
@@ -165,11 +165,7 @@
{:else if tab == 'workspaceflows'}
<WorkspaceFlowList on:pick={(e) => pickFlow(e.detail)} />
{:else if tab == 'hubscripts'}
<PickHubItems
bind:filter
on:pickScript={(e) => pickHubScript(e.detail.path)}
selectedKind="script"
/>
<PickHubScript bind:filter on:pick={(e) => pickHubScript(e.detail.path)} />
{/if}
</div>
</div>

View File

@@ -21,7 +21,7 @@
let badgeClass = 'inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border'
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
@@ -60,6 +60,7 @@
hoveredComponent: undefined
}
$app = $app
$worldStore = $worldStore
}
}

View File

@@ -130,10 +130,6 @@ export type ConnectingInput = {
hoveredComponent: string | undefined
}
export interface CancelablePromise<T> extends Promise<T> {
cancel: () => void
}
export type AppViewerContext = {
worldStore: Writable<World>
app: Writable<App>
@@ -152,7 +148,7 @@ export type AppViewerContext = {
{
autoRefresh: boolean
refreshOnStart?: boolean
cb: (inlineScript?: InlineScript) => CancelablePromise<void>
cb: (inlineScript?: InlineScript) => Promise<void>
}
>
>

View File

@@ -33,15 +33,7 @@
onClick?: () => void
href?: string
}
export let dropdownItems: MenuItem[] | (() => MenuItem[]) | undefined = undefined
function computeDropdowns(): MenuItem[] | undefined {
if (typeof dropdownItems === 'function') {
return dropdownItems()
} else {
return dropdownItems
}
}
export let dropdownItems: MenuItem[] | undefined = undefined
const dispatch = createEventDispatcher()
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
@@ -88,6 +80,18 @@
$: buttonProps = {
id,
class: twMerge(
colorVariants?.[color]?.[variant],
variant === 'border' ? 'border' : '',
ButtonType.FontSizeClasses[size],
ButtonType.SpacingClasses[spacingSize][variant],
'focus:ring-2 font-semibold',
dropdownItems ? 'rounded-l-md h-full' : 'rounded-md',
'justify-center items-center text-center whitespace-nowrap inline-flex',
btnClasses,
disabled ? '!bg-gray-300 !text-gray-600 !cursor-not-allowed' : '',
'transition-all'
),
href,
target,
tabindex: disabled ? -1 : 0,
@@ -116,22 +120,9 @@
$: isSmall = size === 'xs' || size === 'sm'
$: startIconClass = twMerge(iconOnly ? undefined : isSmall ? 'mr-1' : 'mr-2', startIcon?.classes)
$: endIconClass = twMerge(iconOnly ? undefined : isSmall ? 'ml-1' : 'ml-2', endIcon?.classes)
$: buttonClass = twMerge(
'w-full',
colorVariants?.[color]?.[variant],
variant === 'border' ? 'border' : '',
ButtonType.FontSizeClasses[size],
ButtonType.SpacingClasses[spacingSize][variant],
'focus:ring-2 font-semibold',
dropdownItems ? 'rounded-l-md h-full' : 'rounded-md',
'justify-center items-center text-center whitespace-nowrap inline-flex',
btnClasses,
'transition-all '
)
</script>
<div class="{dropdownItems ? ' divide-x divide-frost-600' : ''} {wrapperClasses} flex flex-row">
<div class="{dropdownItems ? 'flex flex-row divide-x divide-frost-600' : ''} {wrapperClasses}">
<svelte:element
this={href ? 'a' : 'button'}
bind:this={element}
@@ -139,7 +130,6 @@
on:click={onClick}
on:focus
on:blur
class={twMerge(buttonClass, disabled ? '!bg-gray-300 !text-gray-600 !cursor-not-allowed' : '')}
{...buttonProps}
disabled={disabled || loading}
type="submit"
@@ -160,14 +150,12 @@
</svelte:element>
{#if dropdownItems}
<div class={twMerge(buttonClass, 'rounded-r-md rounded-l-none m-0 p-0 h-auto')}>
<div class={twMerge(buttonProps.class, 'rounded-r-md rounded-l-none m-0 p-0 h-auto')}>
<ButtonDropdown>
<svelte:fragment slot="items">
{#each computeDropdowns() ?? [] as item}
{#each dropdownItems as item}
<MenuItem on:click={item.onClick} href={item.href}>
<div
class="!text-gray-700 text-left px-4 py-2 my-1 cursor-pointer hover:bg-gray-100 !text-sm"
>
<div class="!text-gray-700 px-4 py-2 my-1 cursor-pointer hover:bg-gray-100 !text-sm">
{item.label}
</div>
</MenuItem>

View File

@@ -100,7 +100,6 @@
z-index: -1;
transition: z-index var(--duration) step-end;
overflow: clip;
pointer-events: none;
}
.drawer.open {
@@ -108,7 +107,6 @@
width: 100%;
z-index: 1002;
transition: z-index var(--duration) step-start;
pointer-events: auto;
}
.overlay {

View File

@@ -16,7 +16,7 @@
<span class="font-semibold truncate text-gray-800">{title ?? ''}</span>
</div>
{#if $$slots.actions}
<div class="flex gap-2 items-center justify-end">
<div class="flex gap-1 items-center justify-end">
<slot name="actions" />
</div>
{/if}

View File

@@ -7,7 +7,7 @@
import type ShareModal from '$lib/components/ShareModal.svelte'
import { FlowService, type Flow } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { isOwner, sendUserToast } from '$lib/utils'
import { sendUserToast } from '$lib/utils'
import {
faArchive,
faCalendarAlt,
@@ -36,13 +36,9 @@
const dispatch = createEventDispatcher()
async function archiveFlow(path: string, archived: boolean): Promise<void> {
async function archiveFlow(path: string): Promise<void> {
try {
await FlowService.archiveFlowByPath({
workspace: $workspaceStore!,
path,
requestBody: { archived }
})
await FlowService.archiveFlowByPath({ workspace: $workspaceStore!, path })
dispatch('change')
sendUserToast(`Archived flow ${path}`)
} catch (err) {
@@ -111,7 +107,7 @@
{/if}
<Button
href="/flows/get/{path}?workspace={$workspaceStore}"
href="/flows/get/{path}?workspace_id={$workspaceStore}"
color="light"
variant="border"
size="xs"
@@ -133,79 +129,76 @@
<Dropdown
placement="bottom-end"
dropdownItems={() => {
let owner = isOwner(path, $userStore, $workspaceStore)
return [
{
displayName: 'View flow',
icon: faEye,
href: `/flows/get/${path}?workspace=${$workspaceStore}`
dropdownItems={[
{
displayName: 'View flow',
icon: faEye,
href: `/flows/get/${path}?workspace_id=${$workspaceStore}`
},
{
displayName: 'Edit',
icon: faEdit,
href: `/flows/edit/${path}?nodraft=true`,
disabled: !canWrite || archived
},
{
displayName: 'Use as template/Fork',
icon: faCodeFork,
href: `/flows/add?template=${path}`
},
{
displayName: 'View runs',
icon: faList,
href: `/runs/${path}`
},
{
displayName: 'Move/Rename',
icon: faFileExport,
action: () => {
moveDrawer.openDrawer(path, summary, 'flow')
},
{
displayName: 'Edit',
icon: faEdit,
href: `/flows/edit/${path}?nodraft=true`,
disabled: !canWrite || archived
disabled: !canWrite || archived
},
{
displayName: 'Schedule',
icon: faCalendarAlt,
action: () => {
scheduleEditor.openNew(true, path)
},
{
displayName: 'Use as template/Fork',
icon: faCodeFork,
href: `/flows/add?template=${path}`
disabled: archived
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: faShare,
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'flow')
}
},
{
displayName: 'Archive',
icon: faArchive,
action: () => {
path ? archiveFlow(path) : null
},
{
displayName: 'View runs',
icon: faList,
href: `/runs/${path}`
},
{
displayName: 'Move/Rename',
icon: faFileExport,
action: () => {
moveDrawer.openDrawer(path, summary, 'flow')
},
disabled: !owner || archived
},
{
displayName: 'Schedule',
icon: faCalendarAlt,
action: () => {
scheduleEditor.openNew(true, path)
},
disabled: archived
},
{
displayName: owner ? 'Share' : 'See Permissions',
icon: faShare,
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'flow')
type: 'delete',
disabled: !canWrite || archived
},
{
displayName: 'Delete',
icon: faTrashAlt,
action: (event) => {
if (event?.shiftKey) {
deleteFlow(path)
} else {
deleteConfirmedCallback = () => {
deleteFlow(path)
}
}
},
{
displayName: archived ? 'Unarchive' : 'Archive',
icon: faArchive,
action: () => {
path && archiveFlow(path, !archived)
},
type: 'delete',
disabled: !owner
},
{
displayName: 'Delete',
icon: faTrashAlt,
action: (event) => {
if (event?.shiftKey) {
deleteFlow(path)
} else {
deleteConfirmedCallback = () => {
deleteFlow(path)
}
}
},
type: 'delete',
disabled: !owner
}
]
}}
type: 'delete',
disabled: !canWrite
}
]}
/>
</svelte:fragment>
</Row>

View File

@@ -8,7 +8,7 @@
import { ScriptService, type Script } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { capitalize, isOwner, sendUserToast } from '$lib/utils'
import { capitalize, sendUserToast } from '$lib/utils'
import {
faArchive,
faCalendarAlt,
@@ -55,20 +55,6 @@
sendUserToast(`Archived script ${path}`)
}
async function unarchiveScript(path: string): Promise<void> {
const r = await ScriptService.getScriptByPath({ workspace: $workspaceStore!, path })
await ScriptService.createScript({
workspace: $workspaceStore!,
requestBody: {
...r,
parent_hash: r.hash,
lock: r.lock?.split('\n')
}
})
dispatch('change')
sendUserToast(`Unarchived script ${path}`)
}
async function deleteScript(path: string): Promise<void> {
await ScriptService.deleteScriptByPath({ workspace: $workspaceStore!, path })
dispatch('change')
@@ -138,7 +124,7 @@
{/if}
<Button
href="/scripts/get/{hash}?workspace={$workspaceStore}"
href="/scripts/get/{hash}?workspace_id={$workspaceStore}"
color="light"
variant="border"
size="xs"
@@ -159,91 +145,88 @@
</span>
<Dropdown
placement="bottom-end"
dropdownItems={() => {
let owner = isOwner(path, $userStore, $workspaceStore)
return [
{
displayName: 'View script',
icon: faEye,
href: `/scripts/get/${hash}?workspace=${$workspaceStore}`
},
dropdownItems={[
{
displayName: 'View script',
icon: faEye,
href: `/scripts/get/${hash}?workspace_id=${$workspaceStore}`
},
{
displayName: 'Edit',
icon: faEdit,
href: `/scripts/edit/${hash}`,
disabled: !canWrite || archived
{
displayName: 'Edit',
icon: faEdit,
href: `/scripts/edit/${hash}`,
disabled: !canWrite || archived
},
{
displayName: 'Edit code',
icon: faEdit,
href: `/scripts/edit/${hash}`,
disabled: !canWrite || archived
},
{
displayName: 'Use as template',
icon: faCodeFork,
href: `/scripts/add?template=${path}`
},
{
displayName: 'Move/Rename',
icon: faFileExport,
action: () => {
moveDrawer.openDrawer(path, summary, 'script')
},
{
displayName: 'Edit code',
icon: faEdit,
href: `/scripts/edit/${hash}`,
disabled: !canWrite || archived
disabled: !canWrite || archived
},
{
displayName: 'View runs',
icon: faList,
href: `/runs/${path}`
},
{
displayName: 'Schedule',
icon: faCalendarAlt,
action: () => {
scheduleEditor.openNew(false, path)
},
{
displayName: 'Use as template',
icon: faCodeFork,
href: `/scripts/add?template=${path}`
disabled: archived
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: faShare,
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'script')
},
{
displayName: 'Move/Rename',
icon: faFileExport,
action: () => {
moveDrawer.openDrawer(path, summary, 'script')
},
disabled: !owner || archived
disabled: archived
},
{
displayName: 'Archive',
icon: faArchive,
action: () => {
path ? archiveScript(path) : null
},
{
displayName: 'View runs',
icon: faList,
href: `/runs/${path}`
},
{
displayName: 'Schedule',
icon: faCalendarAlt,
action: () => {
scheduleEditor.openNew(false, path)
},
disabled: archived
},
{
displayName: owner ? 'Share' : 'See Permissions',
icon: faShare,
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'script')
},
disabled: archived
},
{
displayName: archived ? 'Unarchive' : 'Archive',
icon: faArchive,
action: () => {
archived ? path && unarchiveScript(path) : path && archiveScript(path)
},
type: 'delete',
disabled: !owner
},
...($userStore?.is_admin || $userStore?.is_super_admin
? [
{
displayName: 'Delete',
icon: faTrashAlt,
action: (event) => {
if (event?.shiftKey) {
type: 'delete',
disabled: !canWrite || archived
},
...($userStore?.is_admin || $userStore?.is_super_admin
? []
: [
{
displayName: 'Delete',
icon: faTrashAlt,
action: (event) => {
if (event?.shiftKey) {
deleteScript(path)
} else {
deleteConfirmedCallback = () => {
deleteScript(path)
} else {
deleteConfirmedCallback = () => {
deleteScript(path)
}
}
},
type: dlt,
disabled: !canWrite
}
]
: [])
]
}}
}
},
type: dlt,
disabled: !canWrite
}
])
]}
/>
</svelte:fragment>
</Row>

View File

@@ -7,8 +7,8 @@
import { faBolt, faCheck, faCode } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import FlowScriptPicker from '../pickers/FlowScriptPicker.svelte'
import PickHubScript from '../pickers/PickHubScript.svelte'
import WorkspaceScriptPicker from '../pickers/WorkspaceScriptPicker.svelte'
import PickHubItems from '../pickers/PickHubItems.svelte'
export let failureModule: boolean
export let shouldDisableTriggerScripts: boolean = false
@@ -157,9 +157,9 @@
>Use pre-made <span class="text-blue-500">{kind == 'script' ? 'action' : kind}</span> script</h3
>
{#if pick_existing == 'hub'}
<PickHubItems bind:filter {kind} on:pickScript={() => dispatch('pick')}>
<PickHubScript bind:filter {kind} on:pick>
<ToggleHubWorkspace bind:selected={pick_existing} />
</PickHubItems>
</PickHubScript>
{:else}
<WorkspaceScriptPicker displayLock bind:filter {kind} on:pick>
<ToggleHubWorkspace bind:selected={pick_existing} />

View File

@@ -1,34 +1,81 @@
<script lang="ts">
import { Badge } from '$lib/components/common'
import { createEventDispatcher, onMount } from 'svelte'
import { Badge, Skeleton } from '$lib/components/common'
import SearchItems from '$lib/components/SearchItems.svelte'
import { loadHubApps } from '$lib/utils'
import ListFilters from '$lib/components/home/ListFilters.svelte'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import type { HubApp } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
export let item: HubApp & { marked?: string }
export let filter = ''
type Item = { apps: string[]; summary: string; path: string }
let hubApps: any[] | undefined = undefined
let filteredItems: (Item & { marked?: string })[] = []
let appFilter: string | undefined = undefined
$: prefilteredItems = appFilter
? (hubApps ?? []).filter((i) => i.apps.includes(appFilter))
: hubApps ?? []
$: apps = Array.from(new Set(filteredItems?.flatMap((x) => x.apps) ?? [])).sort()
const dispatch = createEventDispatcher()
onMount(async () => {
hubApps = await loadHubApps()
})
</script>
<button
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<RowIcon kind="app" />
<SearchItems
{filter}
items={prefilteredItems}
bind:filteredItems
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
/>
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input type="text" placeholder="Search Hub Apps" bind:value={filter} class="text-2xl grow" />
</div>
<ListFilters filters={apps} bind:selectedFilter={appFilter} resourceType />
<div class="w-full text-left font-normal">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
</div>
</div>
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
{#each item.apps as app}
<Badge color="gray" baseClass="border">{app}</Badge>
{/each}
</div>
</button>
{#if hubApps}
{#if filteredItems.length == 0}
<NoItemFound />
{:else}
<ul class="divide-y divide-gray-200 border rounded-md">
{#each filteredItems as item (item)}
<li class="flex flex-row w-full">
<button
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<RowIcon kind="app" />
<div class="w-full text-left font-normal ">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
</div>
</div>
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
{#each item.apps as app}
<Badge color="gray" baseClass="border">{app}</Badge>
{/each}
</div>
</button>
</li>
{/each}
</ul>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{/if}

View File

@@ -1,34 +1,82 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Badge } from '$lib/components/common'
import { createEventDispatcher, onMount } from 'svelte'
import { Badge, Skeleton } from '$lib/components/common'
import SearchItems from '$lib/components/SearchItems.svelte'
import { loadHubFlows } from '$lib/utils'
import ListFilters from '$lib/components/home/ListFilters.svelte'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import type { HubFlow } from '$lib/stores'
export let item: HubFlow & { marked?: string }
export let filter = ''
type Item = { apps: string[]; summary: string; path: string }
let hubFlows: any[] | undefined = undefined
let filteredItems: (Item & { marked?: string })[] = []
let appFilter: string | undefined = undefined
$: prefilteredItems = appFilter
? (hubFlows ?? []).filter((i) => i.apps.includes(appFilter))
: hubFlows ?? []
$: apps = Array.from(new Set(filteredItems?.flatMap((x) => x.apps) ?? [])).sort()
const dispatch = createEventDispatcher()
onMount(async () => {
hubFlows = await loadHubFlows()
})
</script>
<button
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<RowIcon kind="flow" />
<SearchItems
{filter}
items={prefilteredItems}
bind:filteredItems
f={(x) => x.summary + ' (' + x.apps.join(', ') + ')'}
/>
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input type="text" placeholder="Search Hub Flows" bind:value={filter} class="text-2xl grow" />
</div>
<ListFilters filters={apps} bind:selectedFilter={appFilter} resourceType />
<div class="w-full text-left font-normal">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
</div>
</div>
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
{#each item.apps as app}
<Badge color="gray" baseClass="border">{app}</Badge>
{/each}
</div>
</button>
{#if hubFlows}
{#if filteredItems.length == 0}
<NoItemFound />
{:else}
<ul class="divide-y divide-gray-200 border rounded-md">
{#each filteredItems as item (item)}
<li class="flex flex-row w-full">
<button
class="p-4 gap-4 flex flex-row grow justify-between hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<RowIcon kind="flow" />
<div class="w-full text-left font-normal ">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
</div>
</div>
<div class="min-w-1/3 gap-2 flex flex-wrap justify-end">
{#each item.apps as app}
<Badge color="gray" baseClass="border">{app}</Badge>
{/each}
</div>
</button>
</li>
{/each}
</ul>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{/if}

View File

@@ -1,150 +0,0 @@
<!-- PickHubItems.svelte -->
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { Skeleton, ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import SearchItems from '$lib/components/SearchItems.svelte'
import { loadHubScripts, loadHubFlows, loadHubApps } from '$lib/utils'
import ListFilters from '$lib/components/home/ListFilters.svelte'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import { hubScripts, hubFlows, hubApps, type HubItem } from '$lib/stores'
import PickHubScript from './PickHubScript.svelte'
import PickHubFlow from './PickHubFlow.svelte'
import PickHubApp from './PickHubApp.svelte'
import { faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
import { Code2, LayoutDashboard } from 'lucide-svelte'
import { Icon } from 'svelte-awesome'
export let filter = ''
export let selectedKind: 'script' | 'flow' | 'app' | undefined = undefined
export let kind: 'script' | 'trigger' | 'approval' | 'failure' | undefined = undefined
let itemKind: 'script' | 'flow' | 'app' = 'app'
const dispatch = createEventDispatcher()
let filteredItems: (HubItem & { marked?: string })[] = []
let appFilter: string | undefined = undefined
$: items = [
...($hubScripts?.map((s) => ({ itemType: 'script', data: s })) ?? []),
...($hubFlows?.map((s) => ({ itemType: 'flow', data: s })) ?? []),
...($hubApps?.map((s) => ({ itemType: 'app', data: s })) ?? [])
] as HubItem[]
$: apps = Array.from(
new Set(
filteredItems
?.map((x) => {
if (x.itemType === 'script') {
return x.data.app
} else if (x.itemType === 'flow') {
return x.data.apps
} else if (x.itemType === 'app') {
return x.data.apps
} else {
return undefined
}
})
.filter(Boolean)
.flat() ?? []
)
).sort() as string[]
$: prefilteredItems =
appFilter || itemKind
? items.filter((i) => {
if (i.itemType !== itemKind) {
return false
}
if (selectedKind && i.itemType !== selectedKind) {
return false
}
if (i.itemType === 'script' && appFilter) {
return i.data.app === appFilter
} else if (i.itemType === 'flow' && appFilter) {
return i.data.apps.includes(appFilter)
} else if (i.itemType === 'app' && appFilter) {
return i.data.apps.includes(appFilter)
}
return true
})
: items
let notLoaded = true
async function loadApps() {
$hubApps = await loadHubApps()
notLoaded = false
}
async function loadFlows() {
loadHubFlows()
}
onMount(async () => {
loadHubScripts()
loadApps()
loadFlows()
})
</script>
<SearchItems {filter} items={prefilteredItems} bind:filteredItems f={(x) => x.data.summary} />
<div class="w-full flex mt-1 items-center gap-2">
{#if selectedKind === undefined}
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButton light position="left" value="script" size="sm">
<div class="flex gap-1 items-center">
<Code2 size={16} />
Scripts
</div>
</ToggleButton>
<ToggleButton light position="center" value="flow" size="sm">
<div class="flex gap-1 items-center">
<Icon data={faBarsStaggered} scale={0.8} class="mr-1" />
Flows
</div>
</ToggleButton>
<ToggleButton light position="right" value="app" size="sm">
<div class="flex gap-1 items-center">
<LayoutDashboard size={16} />
Apps
</div>
</ToggleButton>
</ToggleButtonGroup>
{/if}
<input type="text" placeholder="Search Hub Items" bind:value={filter} class="text-2xl grow" />
<slot />
</div>
<ListFilters filters={apps} bind:selectedFilter={appFilter} resourceType />
{#if Array.isArray(prefilteredItems)}
{#if notLoaded}
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{:else if filteredItems.length == 0}
<NoItemFound />
{:else}
<ul class="divide-y divide-gray-200 border rounded-md">
{#each filteredItems as item, index (index)}
<li class="flex flex-row w-full">
{#if item.itemType === 'script'}
<PickHubScript
item={item.data}
on:pick={() => dispatch('pickScript', item.data)}
{kind}
/>
{:else if item.itemType === 'flow'}
<PickHubFlow item={item.data} on:pick={() => dispatch('pickFlow', item.data)} />
{:else if item.itemType === 'app'}
<PickHubApp item={item.data} on:pick={() => dispatch('pickApp', item.data)} />
{/if}
</li>
{/each}
</ul>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{/if}

View File

@@ -1,44 +1,95 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Badge } from '$lib/components/common'
import { capitalize, classNames } from '$lib/utils'
import { hubScripts } from '$lib/stores'
import { createEventDispatcher, onMount } from 'svelte'
import type { HubItem } from './model'
import { Badge, Skeleton } from '$lib/components/common'
import SearchItems from '$lib/components/SearchItems.svelte'
import { capitalize, classNames, loadHubScripts } from '$lib/utils'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
import type { HubScript } from '$lib/stores'
import ListFilters from '$lib/components/home/ListFilters.svelte'
export let kind: 'script' | 'trigger' | 'approval' | 'failure' = 'script'
export let item: HubScript & { marked?: string }
export let filter = ''
const dispatch = createEventDispatcher()
let filteredItems: (HubItem & { marked?: string })[] = []
let appFilter: string | undefined = undefined
$: items = ($hubScripts ?? []).filter((i) => i.kind === kind)
$: prefilteredItems = appFilter ? (items ?? []).filter((i) => i.app == appFilter) : items ?? []
$: apps = Array.from(new Set(filteredItems?.map((x) => x.app) ?? [])).sort()
onMount(() => {
if (!$hubScripts) {
loadHubScripts()
}
})
const maxItems = 40
</script>
<button
class="p-4 gap-4 flex flex-row grow hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<div
class={classNames(
'rounded-md p-1 flex justify-center items-center border',
'bg-gray-50 border-gray-200'
)}
>
<svelte:component this={APP_TO_ICON_COMPONENT[item['app']]} height={18} width={18} />
</div>
<SearchItems {filter} items={prefilteredItems} bind:filteredItems f={(x) => x.summary} />
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input type="text" placeholder="Search Hub Scripts" bind:value={filter} class="text-2xl grow" />
</div>
<ListFilters filters={apps} bind:selectedFilter={appFilter} resourceType />
<div class="w-full text-left font-normal">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
<div class="text-gray-600 text-xs">
{item.path}
</div>
</div>
</div>
{#if kind !== 'script'}
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
{#if $hubScripts}
{#if filteredItems.length == 0}
<NoItemFound />
{:else}
<ul class="divide-y divide-gray-200 border rounded-md">
{#each filteredItems.slice(0, maxItems) as item (item.path)}
<li class="flex flex-row w-full">
<button
class="p-4 gap-4 flex flex-row grow hover:bg-gray-50 bg-white transition-all items-center rounded-md"
on:click={() => dispatch('pick', item)}
>
<div class="flex items-center gap-4">
<div
class={classNames(
'rounded-md p-1 flex justify-center items-center border',
'bg-gray-50 border-gray-200'
)}
>
<svelte:component
this={APP_TO_ICON_COMPONENT[item['app']]}
height={18}
width={18}
/>
</div>
<div class="w-full text-left font-normal ">
<div class="text-gray-900 flex-wrap text-md font-semibold mb-1">
{#if item.marked}
{@html item.marked ?? ''}
{:else}
{item.summary ?? ''}
{/if}
</div>
<div class="text-gray-600 text-xs ">
{item.path}
</div>
</div>
</div>
{#if kind !== 'script'}
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
{/if}
</button>
</li>
{/each}
</ul>
{/if}
</button>
{#if filteredItems.length > maxItems}
<div class="text-gray-500 text-sm py-4">
There are more items ({filteredItems.length}) than being displayed. Refine your search.
</div>
{/if}
{:else}
<div class="my-2" />
{#each Array(10).fill(0) as _}
<Skeleton layout={[0.5, [4]]} />
{/each}
{/if}

View File

@@ -302,10 +302,7 @@
</div>
<div class="relative">
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
{#if filteredItems?.length == 0}
<div class="mt-10" />
{/if}
{#if !loading}
{#if !loading && filteredItems?.length}
<div class="absolute -bottom-2 right-0 bg-white/90">
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} /></div
>

View File

@@ -28,7 +28,6 @@
import { check } from 'svelte-awesome/icons'
import { Badge } from '../common'
import ScheduleEditor from '../ScheduleEditor.svelte'
import JobPreview from './JobPreview.svelte'
const SMALL_ICON_SCALE = 0.7
@@ -73,7 +72,7 @@
{#if job === undefined}
No job found
{:else}
<div class="block text-center align-middle px-6">
<div class="block text-center align-middle px-6">
{#if 'success' in job && job.success}
{#if job.is_skipped}
<Icon
@@ -122,27 +121,25 @@
</div>
<div class="flex flex-row space-x-2">
<JobPreview {job}>
<div class="whitespace-nowrap">
{#if job.script_path}
<a href="/run/{job.id}?workspace={job.workspace_id}">{job.script_path} </a>
{:else if 'job_kind' in job && job.job_kind == 'preview'}
<a href="/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
<a href="/run/{job.id}?workspace={job.workspace_id}"
>lock deps of {truncateHash(job.script_hash ?? '')}</a
>
{:else if 'job_kind' in job && job.job_kind == 'identity'}
<a href="/run/{job.id}?workspace={job.workspace_id}">no op</a>
{/if}
<button
class="ml-1"
on:click={() => {
goto(`/runs/${job.script_path}?${$page.url.searchParams.toString()}`)
}}><Badge><Icon scale={0.7} data={faSearch} /></Badge></button
<div class="whitespace-nowrap">
{#if job.script_path}
<a href="/run/{job.id}?workspace={job.workspace_id}">{job.script_path} </a>
{:else if 'job_kind' in job && job.job_kind == 'preview'}
<a href="/run/{job.id}?workspace={job.workspace_id}">Preview without path </a>
{:else if 'job_kind' in job && job.job_kind == 'dependencies'}
<a href="/run/{job.id}?workspace={job.workspace_id}"
>lock deps of {truncateHash(job.script_hash ?? '')}</a
>
</div>
</JobPreview>
{:else if 'job_kind' in job && job.job_kind == 'identity'}
<a href="/run/{job.id}?workspace={job.workspace_id}">no op</a>
{/if}
<button
class="ml-1"
on:click={() => {
goto(`/runs/${job.script_path}?${$page.url.searchParams.toString()}`)
}}><Badge><Icon scale={0.7} data={faSearch} /></Badge></button
>
</div>
<div class="whitespace-nowrap">
{#if 'job_kind' in job}<a href="/run/{job.id}"
><Badge color="blue">{job.job_kind}</Badge></a
@@ -157,7 +154,7 @@
</div>
{/if}
</div>
<div class="pl-14 italic text-gray-500 text-2xs whitespace-nowrap overflow-hidden"
<div class="pl-14 italic text-gray-500 text-2xs whitespace-nowrap overflow-hidden"
>{truncateRev(job.id, 8, '')}</div
>
</div>
@@ -200,7 +197,7 @@
<div class="inline-flex gap-1">
<CalendarClock size={13} class="-ml-0.5" />
<span>
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1">Scheduled</span>
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1 ">Scheduled</span>
for {displayDate(job.scheduled_for ?? '')}
</span>
</div>
@@ -209,7 +206,7 @@
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} /><span
class="mx-2"
>
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1"
<span class="bg-blue-200 text-gray-700 text-xs rounded px-1 "
>Waiting for an executor</span
>
</span>

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