Compare commits
44 Commits
v1.74.0
...
app-compon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3373edddf | ||
|
|
71da2f67e6 | ||
|
|
75f87e7e11 | ||
|
|
3e5a179eb8 | ||
|
|
c082c6350e | ||
|
|
cfd489a550 | ||
|
|
1f4ae53fb4 | ||
|
|
0f4da49886 | ||
|
|
ae81d7ee2c | ||
|
|
0dcbf270da | ||
|
|
82c139ed09 | ||
|
|
98f8b845a6 | ||
|
|
f521fb63fa | ||
|
|
fce464434b | ||
|
|
ee18804c87 | ||
|
|
0789bef120 | ||
|
|
1a7dc0a3bd | ||
|
|
ce323709a9 | ||
|
|
61a5e1f1ac | ||
|
|
3b44f9a72c | ||
|
|
b349308ff7 | ||
|
|
6b5963f9d2 | ||
|
|
f87b722a21 | ||
|
|
d00a80a8f6 | ||
|
|
0e9be7f300 | ||
|
|
8681e83b57 | ||
|
|
bc440f8d41 | ||
|
|
1d5c194f09 | ||
|
|
7a9d230459 | ||
|
|
4d5e2499cf | ||
|
|
686275fd46 | ||
|
|
99399f4f77 | ||
|
|
6e09194313 | ||
|
|
7c825c212d | ||
|
|
480fd781b6 | ||
|
|
4f2079f624 | ||
|
|
43c45d930c | ||
|
|
8d5c5b88a3 | ||
|
|
cc8bedd0c7 | ||
|
|
74c3d6443c | ||
|
|
c7be313210 | ||
|
|
ae53bafaf6 | ||
|
|
2ea15d5035 | ||
|
|
0f187d66dd |
20
.github/workflows/deploy_to_windmill.yml
vendored
20
.github/workflows/deploy_to_windmill.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: Deploy to windmill.dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "community/**"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Deploy to windmill.dev
|
||||
uses: windmill-labs/windmill-gh-action-deploy@v2.0.0
|
||||
with:
|
||||
dry_run: false
|
||||
input_dir: community
|
||||
windmill_workspace: starter
|
||||
windmill_token: ${{ secrets.WINDMILL_API_TOKEN }}
|
||||
64
.github/workflows/docker-image.yml
vendored
64
.github/workflows/docker-image.yml
vendored
@@ -109,38 +109,38 @@ jobs:
|
||||
${{ steps.meta-ee-public.outputs.labels }}
|
||||
org.opencontainers.image.licenses=Windmill-Enterprise-License
|
||||
|
||||
|
||||
playwright:
|
||||
runs-on: [self-hosted, new]
|
||||
needs: [build]
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_USER: admin
|
||||
POSTGRES_PASSWORD: changeme
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Docker"
|
||||
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
|
||||
id: docker-container
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: "Playwright run"
|
||||
timeout-minutes: 2
|
||||
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
|
||||
- name: "Clean up"
|
||||
run: docker kill ${{ steps.docker-container.outputs.id }}
|
||||
if: always()
|
||||
# disabled until we make it 100% reliable and add more meaningful tests
|
||||
# playwright:
|
||||
# runs-on: [self-hosted, new]
|
||||
# needs: [build]
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres
|
||||
# env:
|
||||
# POSTGRES_DB: windmill
|
||||
# POSTGRES_USER: admin
|
||||
# POSTGRES_PASSWORD: changeme
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - name: "Docker"
|
||||
# run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
|
||||
# id: docker-container
|
||||
# - uses: actions/setup-node@v3
|
||||
# with:
|
||||
# node-version: 16
|
||||
# - name: "Playwright run"
|
||||
# timeout-minutes: 2
|
||||
# run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
|
||||
# - name: "Clean up"
|
||||
# run: docker kill ${{ steps.docker-container.outputs.id }}
|
||||
# if: always()
|
||||
|
||||
|
||||
publish_privately_heavy:
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,6 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
|
||||
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
|
||||
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
|
||||
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
|
||||
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
|
||||
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
|
||||
|
||||
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** fix splitpanes navigation ([#1276](https://github.com/windmill-labs/windmill/issues/1276)) ([8d5c5b8](https://github.com/windmill-labs/windmill/commit/8d5c5b88a35d7a3bad1d8ddf2d940026825241eb))
|
||||
|
||||
## [1.74.1](https://github.com/windmill-labs/windmill/compare/v1.74.0...v1.74.1) (2023-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** proper reactivity for non rendered static components ([ae53baf](https://github.com/windmill-labs/windmill/commit/ae53bafaf6777f928113f84b2c6ed6a2ed341844))
|
||||
* **ci:** make windmill compile again by pinning swc deps ([2ea15d5](https://github.com/windmill-labs/windmill/commit/2ea15d5035e5e15473968db3c0501a4dddff5cd0))
|
||||
|
||||
## [1.74.0](https://github.com/windmill-labs/windmill/compare/v1.73.1...v1.74.0) (2023-03-09)
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ ARG features=""
|
||||
|
||||
COPY --from=planner /windmill/recipe.json recipe.json
|
||||
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef cook --release --features "$features" --recipe-path recipe.json
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
|
||||
|
||||
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
|
||||
COPY ./backend ./
|
||||
|
||||
93
backend/Cargo.lock
generated
93
backend/Cargo.lock
generated
@@ -1123,9 +1123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
|
||||
checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1138,9 +1138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
|
||||
checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1148,15 +1148,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
|
||||
checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
|
||||
checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1176,9 +1176,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
|
||||
checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -1197,9 +1197,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
|
||||
checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1208,21 +1208,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
|
||||
checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
|
||||
checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.26"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
|
||||
checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
@@ -1503,9 +1503,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.24"
|
||||
version = "0.14.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
|
||||
checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -2953,7 +2953,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-ast"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#1fceeab0fcc77c11dfe94092b27b2164b94d62e4"
|
||||
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"rustpython-compiler-core",
|
||||
@@ -2962,7 +2962,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-compiler-core"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#1fceeab0fcc77c11dfe94092b27b2164b94d62e4"
|
||||
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bstr",
|
||||
@@ -2975,7 +2975,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rustpython-parser"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/RustPython/RustPython#1fceeab0fcc77c11dfe94092b27b2164b94d62e4"
|
||||
source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -3667,9 +3667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "swc_common"
|
||||
version = "0.29.35"
|
||||
version = "0.29.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d515be281f603cb97afaa89896aca5e5c748fde11c7f926e35cdaa8ff8da705"
|
||||
checksum = "5005cd73617e18592faa31298225b26f1c407b84a681d67efb735c3d3458e101"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"ast_node",
|
||||
@@ -3694,9 +3694,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_ast"
|
||||
version = "0.99.0"
|
||||
version = "0.98.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06fb0d52a72db367f9bc9194ada9dceab809fe65d2974b5832e1d1264702fd3a"
|
||||
checksum = "305d2aa5e36a775e0d376552b35b325fccf0d7d8c633da1e4aec8e16460251cf"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"is-macro",
|
||||
@@ -3711,9 +3711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "swc_ecma_parser"
|
||||
version = "0.129.0"
|
||||
version = "0.128.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e9d50f3ed033037b795be7f371e0b1246dc9e910a8aa3adfd6654cda6b21e1b"
|
||||
checksum = "35e349e3f4c5561645b9042caae162dbaf55502be7b583ac99f3ccf3e65bccb7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"enum_kind",
|
||||
@@ -4052,9 +4052,9 @@ checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
|
||||
checksum = "7082a95d48029677a28f181e5f6422d0c8339ad8396a39d3f33d62a90c1f6c30"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
@@ -4274,7 +4274,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
[[package]]
|
||||
name = "typify"
|
||||
version = "0.0.11-dev"
|
||||
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
|
||||
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
|
||||
dependencies = [
|
||||
"typify-impl",
|
||||
"typify-macro",
|
||||
@@ -4283,7 +4283,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typify-impl"
|
||||
version = "0.0.11-dev"
|
||||
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
|
||||
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"log",
|
||||
@@ -4301,7 +4301,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typify-macro"
|
||||
version = "0.0.11-dev"
|
||||
source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4"
|
||||
source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4742,7 +4742,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4769,7 +4769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -4824,7 +4824,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"chrono",
|
||||
@@ -4839,7 +4839,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -4852,7 +4852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -4877,7 +4877,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4885,13 +4885,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"phf",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"unicode-general-category",
|
||||
"windmill-common",
|
||||
"windmill-parser",
|
||||
@@ -4899,7 +4900,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4911,7 +4912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
@@ -4926,7 +4927,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deno_core",
|
||||
@@ -4940,7 +4941,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -4963,7 +4964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -123,8 +123,8 @@ regex = "^1"
|
||||
deno_core = "^0"
|
||||
async-recursion = "^1"
|
||||
swc_common = "^0"
|
||||
swc_ecma_parser = "^0"
|
||||
swc_ecma_ast = "^0"
|
||||
swc_ecma_parser = "0.128.2"
|
||||
swc_ecma_ast = "0.98.1"
|
||||
base64 = "0.21.0"
|
||||
unicode-general-category = "^0"
|
||||
hmac = "0.12.1"
|
||||
|
||||
@@ -16,4 +16,5 @@ unicode-general-category.workspace = true
|
||||
itertools.workspace = true
|
||||
anyhow.workspace = true
|
||||
regex.workspace = true
|
||||
lazy_static.workspace = true
|
||||
lazy_static.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -1,6 +1,8 @@
|
||||
#![allow(non_snake_case)] // TODO: switch to parse_* function naming
|
||||
|
||||
use anyhow::anyhow;
|
||||
use regex::Regex;
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
@@ -17,19 +19,32 @@ pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result<MainArgSigna
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+):-(.*)\})"$"#).unwrap();
|
||||
}
|
||||
|
||||
fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut hm = HashMap::new();
|
||||
let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap();
|
||||
for cap in re.captures_iter(code) {
|
||||
hm.insert(cap[2].parse::<i32>()?, cap[1].to_string());
|
||||
let mut hm: HashMap<i32, (String, Option<String>)> = HashMap::new();
|
||||
for cap in RE.captures_iter(code) {
|
||||
hm.insert(
|
||||
cap.get(2)
|
||||
.or(cap.get(3))
|
||||
.and_then(|x| x.as_str().parse::<i32>().ok())
|
||||
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
|
||||
(
|
||||
cap[1].to_string(),
|
||||
cap.get(4).map(|x| x.as_str().to_string()),
|
||||
),
|
||||
);
|
||||
}
|
||||
let mut args = vec![];
|
||||
for i in 1..20 {
|
||||
if hm.contains_key(&i) {
|
||||
let (name, default) = hm.get(&i).unwrap();
|
||||
args.push(Arg {
|
||||
name: hm[&i].clone(),
|
||||
name: name.clone(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
default: default.clone().map(|x| json!(x)),
|
||||
otyp: None,
|
||||
has_default: false,
|
||||
});
|
||||
@@ -43,6 +58,8 @@ fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -50,8 +67,7 @@ mod tests {
|
||||
let code = r#"
|
||||
token="$1"
|
||||
image="$2"
|
||||
digest="${3:-latest}"
|
||||
foo="$4"
|
||||
digest="${3:-latest with spaces}"
|
||||
|
||||
"#;
|
||||
//println!("{}", serde_json::to_string()?);
|
||||
@@ -74,6 +90,13 @@ foo="$4"
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "digest".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: Some(json!("latest with spaces")),
|
||||
has_default: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2984,20 +2984,6 @@
|
||||
},
|
||||
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
|
||||
},
|
||||
"83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"8543f029d9784234e4c6a6dcd7b03e62d544b98be261334ee210594e0bb839f2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3168,6 +3154,21 @@
|
||||
},
|
||||
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage"
|
||||
},
|
||||
"8c0131a9cc61f2daa258d49767242bcaab6bb34a977ff7fb0c18aa9202d11f47": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4"
|
||||
},
|
||||
"8c11511a74a41a65f448249a00ebe6964a61d00c2f7b4875a55e64741bf1f0ca": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4697,6 +4698,27 @@
|
||||
},
|
||||
"query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE"
|
||||
},
|
||||
"be1de9116b18a40681cb8c5ec7578fd7c5f7ce77f7af63f766c2899d85fe0bef": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "value",
|
||||
"ordinal": 0,
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -5040,6 +5062,18 @@
|
||||
},
|
||||
"query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 UNION ALL SELECT parent_job FROM completed_job WHERE id = $1)\n FOR UPDATE\n "
|
||||
},
|
||||
"c9d97800eb0ec87df8e8959b283dacb2c6cce422365ed394375641488ceb6b65": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE worker_ping SET ping_at = now() WHERE worker = $1"
|
||||
},
|
||||
"cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.74.0
|
||||
version: 1.75.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -3168,12 +3168,14 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/Success"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
responses:
|
||||
"200":
|
||||
description: All available queued jobs
|
||||
@@ -3198,10 +3200,12 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/Success"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -3235,9 +3239,11 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -4614,15 +4620,15 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
CreatedBefore:
|
||||
name: created_before
|
||||
StartedBefore:
|
||||
name: started_before
|
||||
description: filter on created before (inclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedAfter:
|
||||
name: created_after
|
||||
StartedAfter:
|
||||
name: started_after
|
||||
description: filter on created after (exclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
@@ -4646,6 +4652,19 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
ArgsFilter:
|
||||
name: args
|
||||
description: filter on jobs containing those args as a json subset (@> in postgres)
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
|
||||
ResultFilter:
|
||||
name: result
|
||||
description: filter on jobs containing those result as a json subset (@> in postgres)
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
After:
|
||||
name: after
|
||||
description: filter on created after (exclusive) timestamp
|
||||
|
||||
@@ -733,6 +733,9 @@ fn build_args(
|
||||
path: String,
|
||||
args: &Map<String, Value>,
|
||||
) -> Result<Map<String, Value>> {
|
||||
// disallow var and res access in args coming from the user for security reasons
|
||||
args.into_iter()
|
||||
.try_for_each(|x| disallow_var_res_access(x.1))?;
|
||||
let static_args = policy
|
||||
.triggerables
|
||||
.get(&path)
|
||||
@@ -753,3 +756,20 @@ fn build_args(
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn disallow_var_res_access(args: &serde_json::Value) -> Result<()> {
|
||||
match args {
|
||||
Value::Object(v) => v.into_iter().try_for_each(|x| disallow_var_res_access(x.1)),
|
||||
Value::Array(arr) => arr.into_iter().try_for_each(|v| disallow_var_res_access(v)),
|
||||
Value::String(s) => {
|
||||
if s.starts_with("$var:") || s.starts_with("$res:") {
|
||||
Err(Error::BadRequest(format!(
|
||||
"For security reasons, variable or resource access is not allowed as dynamic argument"
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,8 +640,6 @@ mod tests {
|
||||
"type": "script",
|
||||
"path": "test"
|
||||
},
|
||||
"stop_after_if": null,
|
||||
"summary": null
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
@@ -650,15 +648,12 @@ mod tests {
|
||||
"input_transforms": {},
|
||||
"type": "rawscript",
|
||||
"content": "test",
|
||||
"lock": null,
|
||||
"path": null,
|
||||
"language": "deno"
|
||||
},
|
||||
"stop_after_if": {
|
||||
"expr": "foo = 'bar'",
|
||||
"skip_if_stopped": false
|
||||
},
|
||||
"summary": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
@@ -680,8 +675,7 @@ mod tests {
|
||||
"stop_after_if": {
|
||||
"expr": "previous.isEmpty()",
|
||||
"skip_if_stopped": false,
|
||||
},
|
||||
"summary": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"failure_module": {
|
||||
@@ -695,8 +689,7 @@ mod tests {
|
||||
"stop_after_if": {
|
||||
"expr": "previous.isEmpty()",
|
||||
"skip_if_stopped": false
|
||||
},
|
||||
"summary": null
|
||||
}
|
||||
}
|
||||
});
|
||||
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
|
||||
|
||||
@@ -16,10 +16,9 @@ use axum::{
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{self, Error, JsonResult, Result},
|
||||
error::{self, to_anyhow, Error, JsonResult, Result},
|
||||
users::username_to_permissioned_as,
|
||||
utils::{not_found_if_none, paginate, Pagination},
|
||||
};
|
||||
@@ -262,13 +261,26 @@ async fn update_folder(
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
|
||||
if let Some(display_name) = ng.display_name {
|
||||
sqlb.set("display_name", display_name);
|
||||
sqlb.set("display_name", "?".bind(&display_name));
|
||||
}
|
||||
if let Some(owners) = ng.owners {
|
||||
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
|
||||
sqlb.set(
|
||||
"owners",
|
||||
"?".bind(&format!(
|
||||
"{{{}}}",
|
||||
owners
|
||||
.iter()
|
||||
.map(|x| format!("\"{x}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
)),
|
||||
);
|
||||
}
|
||||
if let Some(extra_perms) = ng.extra_perms {
|
||||
sqlb.set_str("extra_perms", extra_perms.to_string());
|
||||
sqlb.set(
|
||||
"extra_perms",
|
||||
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
|
||||
);
|
||||
}
|
||||
|
||||
sqlb.returning("*");
|
||||
|
||||
@@ -327,13 +327,15 @@ pub struct ListQueueQuery {
|
||||
pub script_path_exact: Option<String>,
|
||||
pub script_hash: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub running: Option<bool>,
|
||||
pub parent_job: Option<String>,
|
||||
pub order_desc: Option<bool>,
|
||||
pub job_kinds: Option<String>,
|
||||
pub suspended: Option<bool>,
|
||||
// filter by matching a subset of the args using base64 encoded json subset
|
||||
pub args: Option<String>,
|
||||
}
|
||||
|
||||
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
|
||||
@@ -362,11 +364,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
if let Some(pj) = &lq.parent_job {
|
||||
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
||||
}
|
||||
if let Some(dt) = &lq.created_before {
|
||||
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.started_before {
|
||||
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.started_after {
|
||||
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
|
||||
if let Some(s) = &lq.suspended {
|
||||
@@ -376,6 +378,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
sqlb.and_where_eq("suspend", 0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(jk) = &lq.job_kinds {
|
||||
sqlb.and_where_in(
|
||||
"job_kind",
|
||||
@@ -383,6 +386,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(args) = &lq.args {
|
||||
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||
}
|
||||
|
||||
sqlb
|
||||
}
|
||||
|
||||
@@ -455,13 +462,14 @@ async fn list_jobs(
|
||||
script_path_exact: lq.script_path_exact,
|
||||
script_hash: lq.script_hash,
|
||||
created_by: lq.created_by,
|
||||
created_before: lq.created_before,
|
||||
created_after: lq.created_after,
|
||||
started_before: lq.started_before,
|
||||
started_after: lq.started_after,
|
||||
running: None,
|
||||
parent_job: lq.parent_job,
|
||||
order_desc: Some(true),
|
||||
job_kinds: lq.job_kinds,
|
||||
suspended: lq.suspended,
|
||||
args: lq.args,
|
||||
},
|
||||
&[
|
||||
"'QueuedJob' as typ",
|
||||
@@ -1136,15 +1144,15 @@ where
|
||||
struct InPayload {
|
||||
payload: Option<String>,
|
||||
}
|
||||
|
||||
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
|
||||
let vec = base64::engine::general_purpose::URL_SAFE
|
||||
.decode(t)
|
||||
.context("invalid base64")?;
|
||||
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
|
||||
let vec = base64::engine::general_purpose::URL_SAFE
|
||||
.decode(t)
|
||||
.context("invalid base64")?;
|
||||
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
||||
}
|
||||
pub async fn run_flow_by_path(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -1687,11 +1695,11 @@ fn list_completed_jobs_query(
|
||||
if let Some(pj) = &lq.parent_job {
|
||||
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
||||
}
|
||||
if let Some(dt) = &lq.created_before {
|
||||
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.started_before {
|
||||
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.started_after {
|
||||
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(sk) = &lq.is_skipped {
|
||||
sqlb.and_where_eq("is_skipped", sk);
|
||||
@@ -1706,6 +1714,15 @@ fn list_completed_jobs_query(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(args) = &lq.args {
|
||||
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||
}
|
||||
|
||||
if let Some(result) = &lq.result {
|
||||
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
|
||||
}
|
||||
|
||||
tracing::info!("{:?}", sqlb.sql());
|
||||
sqlb
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -1714,8 +1731,8 @@ pub struct ListCompletedQuery {
|
||||
pub script_path_exact: Option<String>,
|
||||
pub script_hash: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub success: Option<bool>,
|
||||
pub parent_job: Option<String>,
|
||||
pub order_desc: Option<bool>,
|
||||
@@ -1723,6 +1740,10 @@ pub struct ListCompletedQuery {
|
||||
pub is_skipped: Option<bool>,
|
||||
pub is_flow_step: Option<bool>,
|
||||
pub suspended: Option<bool>,
|
||||
// filter by matching a subset of the args using base64 encoded json subset
|
||||
pub args: Option<String>,
|
||||
// filter by matching a subset of the result using base64 encoded json subset
|
||||
pub result: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_completed_jobs(
|
||||
|
||||
@@ -26,6 +26,7 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use serde_json::Value;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{Error, JsonResult, Result},
|
||||
@@ -378,9 +379,26 @@ async fn update_variable(
|
||||
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,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await?;
|
||||
|
||||
if let Some(old_v) = v {
|
||||
v = Some(replace_path(
|
||||
old_v,
|
||||
&format!("$var:{path}"),
|
||||
&format!("$var:{npath}"),
|
||||
))
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
|
||||
npath,
|
||||
v,
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
@@ -419,6 +437,23 @@ async fn update_variable(
|
||||
Ok(format!("variable {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
|
||||
match v {
|
||||
Value::Object(v) => Value::Object(
|
||||
v.into_iter()
|
||||
.map(|(k, v)| (k, replace_path(v, path, npath)))
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(arr) => Value::Array(
|
||||
arr.into_iter()
|
||||
.map(|v| replace_path(v, path, npath))
|
||||
.collect(),
|
||||
),
|
||||
Value::String(s) if s == path => Value::String(npath.to_owned()),
|
||||
_ => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_crypt<'c>(
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
|
||||
@@ -1073,6 +1073,7 @@ struct ScriptMetadata {
|
||||
schema: Option<Schema>,
|
||||
is_template: bool,
|
||||
lock: Vec<String>,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
enum ArchiveImpl {
|
||||
@@ -1235,6 +1236,7 @@ async fn tarball_workspace(
|
||||
description: script.description,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
kind: script.kind.to_string(),
|
||||
lock,
|
||||
};
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt::Display,
|
||||
fmt::{self, Display},
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
@@ -103,6 +103,18 @@ pub enum ScriptKind {
|
||||
Approval,
|
||||
}
|
||||
|
||||
impl Display for ScriptKind {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str(match self {
|
||||
ScriptKind::Trigger => "trigger",
|
||||
ScriptKind::Failure => "failure",
|
||||
ScriptKind::Script => "script",
|
||||
ScriptKind::Approval => "approval",
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
pub struct Script {
|
||||
|
||||
@@ -34,7 +34,7 @@ use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
process::{Child, Command},
|
||||
sync::{
|
||||
mpsc::{self, Sender}, watch,
|
||||
mpsc::{self, Sender}, watch, broadcast,
|
||||
},
|
||||
time::{interval, sleep, Instant, MissedTickBehavior},
|
||||
};
|
||||
@@ -840,7 +840,7 @@ async fn handle_queued_job(
|
||||
job_dir: &str,
|
||||
metrics: Metrics,
|
||||
same_worker_tx: Sender<Uuid>,
|
||||
base_internal_url: &str
|
||||
base_internal_url: &str,
|
||||
) -> windmill_common::error::Result<()> {
|
||||
if job.canceled {
|
||||
return Err(Error::JsonErr(canceled_job_to_result(&job)))?;
|
||||
@@ -888,10 +888,10 @@ async fn handle_queued_job(
|
||||
logs.push_str(&format!("job {} on worker {}\n", &job.id, &worker_name));
|
||||
let result = match job.job_kind {
|
||||
JobKind::Dependencies => {
|
||||
handle_dependency_job(&job, &mut logs, job_dir, db).await
|
||||
handle_dependency_job(&job, &mut logs, job_dir, db, worker_name).await
|
||||
}
|
||||
JobKind::FlowDependencies => {
|
||||
handle_flow_dependency_job(&job, &mut logs, job_dir, db)
|
||||
handle_flow_dependency_job(&job, &mut logs, job_dir, db, worker_name)
|
||||
.await
|
||||
.map(|()| Value::Null)
|
||||
}
|
||||
@@ -912,7 +912,8 @@ async fn handle_queued_job(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
&mut logs,
|
||||
base_internal_url
|
||||
base_internal_url,
|
||||
worker_name
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1064,7 +1065,9 @@ async fn handle_code_execution_job(
|
||||
job_dir: &str,
|
||||
worker_dir: &str,
|
||||
logs: &mut String,
|
||||
base_internal_url: &str
|
||||
base_internal_url: &str,
|
||||
worker_name: &str
|
||||
|
||||
) -> error::Result<serde_json::Value> {
|
||||
let (inner_content, requirements_o, language) = match job.job_kind {
|
||||
JobKind::Preview | JobKind::Script_Hub => (
|
||||
@@ -1086,7 +1089,6 @@ async fn handle_code_execution_job(
|
||||
"handle_code_execution_job should never be reachable with a non-code execution job"
|
||||
),
|
||||
};
|
||||
let worker_name = worker_dir.split("/").last().unwrap_or("unknown");
|
||||
let lang_str = job
|
||||
.language
|
||||
.as_ref()
|
||||
@@ -1136,7 +1138,7 @@ mount {{
|
||||
token,
|
||||
&inner_content,
|
||||
&shared_mount,
|
||||
base_internal_url
|
||||
base_internal_url,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1151,7 +1153,8 @@ mount {{
|
||||
&inner_content,
|
||||
&shared_mount,
|
||||
requirements_o,
|
||||
base_internal_url
|
||||
base_internal_url,
|
||||
worker_name
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1166,7 +1169,8 @@ mount {{
|
||||
job_dir,
|
||||
requirements_o,
|
||||
&shared_mount,
|
||||
base_internal_url
|
||||
base_internal_url,
|
||||
worker_name
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1179,7 +1183,8 @@ mount {{
|
||||
&inner_content,
|
||||
job_dir,
|
||||
&shared_mount,
|
||||
base_internal_url
|
||||
base_internal_url,
|
||||
worker_name
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1208,6 +1213,7 @@ async fn handle_go_job(
|
||||
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");
|
||||
@@ -1236,6 +1242,7 @@ async fn handle_go_job(
|
||||
db,
|
||||
true,
|
||||
skip_go_mod,
|
||||
worker_name
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1353,7 +1360,7 @@ func Run(req Req) (interface{{}}, error){{
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(&job.id, db, logs, build_go, false).await?;
|
||||
handle_child(&job.id, db, logs, build_go, false, worker_name).await?;
|
||||
|
||||
Command::new(NSJAIL_PATH.as_str())
|
||||
.current_dir(job_dir)
|
||||
@@ -1379,7 +1386,7 @@ func Run(req Req) (interface{{}}, error){{
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1393,6 +1400,7 @@ async fn handle_bash_job(
|
||||
job_dir: &str,
|
||||
shared_mount: &str,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
logs.push_str("\n\n--- BASH CODE EXECUTION ---\n");
|
||||
set_logs(logs, &job.id, db).await;
|
||||
@@ -1456,7 +1464,7 @@ async fn handle_bash_job(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
//for now bash jobs have an empty result object
|
||||
Ok(serde_json::json!(logs
|
||||
.lines()
|
||||
@@ -1484,7 +1492,8 @@ async fn handle_deno_job(
|
||||
inner_content: &String,
|
||||
shared_mount: &str,
|
||||
lockfile: Option<String>,
|
||||
base_internal_url: &str
|
||||
base_internal_url: &str,
|
||||
worker_name: &str
|
||||
) -> error::Result<serde_json::Value> {
|
||||
logs.push_str("\n\n--- DENO CODE EXECUTION ---\n");
|
||||
set_logs(logs, &job.id, db).await;
|
||||
@@ -1619,7 +1628,7 @@ run().catch(async (e) => {{
|
||||
}
|
||||
.instrument(trace_span!("create_deno_jail"))
|
||||
.await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1657,7 +1666,7 @@ async fn handle_python_job(
|
||||
token: String,
|
||||
inner_content: &String,
|
||||
shared_mount: &str,
|
||||
base_internal_url: &str
|
||||
base_internal_url: &str,
|
||||
) -> error::Result<serde_json::Value> {
|
||||
create_dependencies_dir(job_dir).await;
|
||||
|
||||
@@ -1671,7 +1680,7 @@ async fn handle_python_job(
|
||||
if requirements.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
pip_compile(&job.id, &requirements, logs, job_dir, db)
|
||||
pip_compile(&job.id, &requirements, logs, job_dir, db, worker_name)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
|
||||
@@ -1888,7 +1897,7 @@ mount {{
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1918,6 +1927,7 @@ async fn handle_dependency_job(
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
worker_name: &str,
|
||||
) -> error::Result<serde_json::Value> {
|
||||
let content = capture_dependency_job(
|
||||
&job.id,
|
||||
@@ -1932,7 +1942,8 @@ async fn handle_dependency_job(
|
||||
.unwrap_or_else(|| "no raw code"),
|
||||
logs,
|
||||
job_dir,
|
||||
db
|
||||
db,
|
||||
worker_name
|
||||
)
|
||||
.await;
|
||||
match content {
|
||||
@@ -1967,6 +1978,7 @@ async fn handle_flow_dependency_job(
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
worker_name: &str,
|
||||
) -> error::Result<()> {
|
||||
let path = job.script_path.clone().ok_or_else(|| {
|
||||
error::Error::InternalErr(
|
||||
@@ -1997,6 +2009,7 @@ async fn handle_flow_dependency_job(
|
||||
logs,
|
||||
job_dir,
|
||||
db,
|
||||
worker_name
|
||||
)
|
||||
.await;
|
||||
match new_lock {
|
||||
@@ -2111,12 +2124,13 @@ async fn capture_dependency_job(
|
||||
job_raw_code: &str,
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
worker_name: &str
|
||||
) -> error::Result<String> {
|
||||
match job_language {
|
||||
ScriptLang::Python3 => {
|
||||
create_dependencies_dir(job_dir).await;
|
||||
pip_compile(job_id, job_raw_code, logs, job_dir, db ).await
|
||||
pip_compile(job_id, job_raw_code, logs, job_dir, db, worker_name).await
|
||||
}
|
||||
ScriptLang::Go => {
|
||||
install_go_dependencies(
|
||||
@@ -2127,6 +2141,7 @@ async fn capture_dependency_job(
|
||||
db,
|
||||
false,
|
||||
false,
|
||||
worker_name
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2144,6 +2159,7 @@ async fn pip_compile(
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &Pool<Postgres>,
|
||||
worker_name: &str
|
||||
) -> error::Result<String> {
|
||||
logs.push_str(&format!("\nresolving dependencies..."));
|
||||
set_logs(logs, job_id, db).await;
|
||||
@@ -2176,7 +2192,7 @@ async fn pip_compile(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(job_id, db, logs, child, false)
|
||||
handle_child(job_id, db, logs, child, false, worker_name)
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
|
||||
let path_lock = format!("{job_dir}/requirements.txt");
|
||||
@@ -2199,6 +2215,7 @@ async fn install_go_dependencies(
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
preview: bool,
|
||||
skip_go_mod: bool,
|
||||
worker_name: &str
|
||||
) -> error::Result<String> {
|
||||
if !skip_go_mod {
|
||||
gen_go_mymod(code, job_dir).await?;
|
||||
@@ -2209,7 +2226,7 @@ async fn install_go_dependencies(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
handle_child(job_id, db, logs, child, false).await?;
|
||||
handle_child(job_id, db, logs, child, false, worker_name).await?;
|
||||
}
|
||||
let child = Command::new(GO_PATH.as_str())
|
||||
.current_dir(job_dir)
|
||||
@@ -2217,7 +2234,7 @@ async fn install_go_dependencies(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(job_id, db, logs, child, false)
|
||||
handle_child(job_id, db, logs, child, false, worker_name)
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
|
||||
|
||||
@@ -2323,6 +2340,7 @@ async fn handle_child(
|
||||
logs: &mut String,
|
||||
mut child: Child,
|
||||
nsjail: bool,
|
||||
worker_name: &str,
|
||||
) -> error::Result<()> {
|
||||
let update_job_interval = Duration::from_millis(500);
|
||||
let write_logs_delay = Duration::from_millis(500);
|
||||
@@ -2336,11 +2354,14 @@ async fn handle_child(
|
||||
tracing::info!("could not get child pid");
|
||||
}
|
||||
let (set_too_many_logs, mut too_many_logs) = watch::channel::<bool>(false);
|
||||
let (tx, mut rx) = broadcast::channel::<()>(3);
|
||||
let mut rx2 = tx.subscribe();
|
||||
|
||||
|
||||
let output = child_joined_output_stream(&mut child);
|
||||
|
||||
let job_id = job_id.clone();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<()>(1);
|
||||
|
||||
/* the cancellation future is polled on by `wait_on_child` while
|
||||
* waiting for the child to exit normally */
|
||||
@@ -2350,10 +2371,22 @@ async fn handle_child(
|
||||
let mut interval = interval(update_job_interval);
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let mut i = 1;
|
||||
loop {
|
||||
tokio::select!(
|
||||
_ = rx.recv() => break,
|
||||
_ = interval.tick() => {
|
||||
// update the last_ping column every 5 seconds
|
||||
i+=1;
|
||||
if i % 10 == 0 {
|
||||
sqlx::query!(
|
||||
"UPDATE worker_ping SET ping_at = now() WHERE worker = $1",
|
||||
&worker_name
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.expect("update worker ping");
|
||||
}
|
||||
let mem_peak = get_mem_peak(pid, nsjail).await;
|
||||
tracing::info!("{job_id} still running. mem peak: {}kB", mem_peak);
|
||||
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
|
||||
@@ -2387,10 +2420,10 @@ async fn handle_child(
|
||||
biased;
|
||||
result = child.wait() => return result.map(Ok),
|
||||
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
|
||||
_ = update_job => KillReason::Cancelled,
|
||||
_ = sleep(*TIMEOUT_DURATION) => KillReason::Timeout,
|
||||
_ = update_job => KillReason::Cancelled,
|
||||
};
|
||||
tx.send(()).await.expect("rx should never be dropped");
|
||||
tx.send(()).expect("rx should never be dropped");
|
||||
drop(tx);
|
||||
|
||||
let set_reason = async {
|
||||
@@ -2413,7 +2446,7 @@ async fn handle_child(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* send SIGKILL and reap child process */
|
||||
let (_, kill) = future::join(set_reason, child.kill()).await;
|
||||
kill.map(|()| Err(kill_reason))
|
||||
@@ -2429,12 +2462,13 @@ async fn handle_child(
|
||||
/* log_remaining is zero when output limit was reached */
|
||||
let mut log_remaining = max_log_size.saturating_sub(logs.chars().count());
|
||||
let mut result = io::Result::Ok(());
|
||||
let mut output = output;
|
||||
let mut output = output.take_until(rx2.recv()).boxed();
|
||||
/* `do_write` resolves the task, but does not contain the Result.
|
||||
* It's useful to know if the task completed. */
|
||||
let (mut do_write, mut write_result) = tokio::spawn(ready(())).remote_handle();
|
||||
|
||||
while let Some(line) = output.by_ref().next().await {
|
||||
while let Some(line) = output.by_ref().next().await {
|
||||
|
||||
let do_write_ = do_write.shared();
|
||||
|
||||
let mut read_lines = stream::once(async { line })
|
||||
@@ -2449,11 +2483,14 @@ async fn handle_child(
|
||||
let mut joined = String::new();
|
||||
|
||||
while let Some(line) = read_lines.next().await {
|
||||
|
||||
match line {
|
||||
Ok(_) if log_remaining == 0 => (),
|
||||
Ok(line) => {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
append_with_limit(&mut joined, &line, &mut log_remaining);
|
||||
|
||||
if log_remaining == 0 {
|
||||
tracing::info!(%job_id, "Too many logs lines for job {job_id}");
|
||||
let _ = set_too_many_logs.send(true);
|
||||
@@ -2473,6 +2510,7 @@ async fn handle_child(
|
||||
|
||||
logs.push_str(&joined);
|
||||
|
||||
|
||||
/* Ensure the last flush completed before starting a new one.
|
||||
*
|
||||
* This shouldn't pause since `take_until()` reads lines until `do_write`
|
||||
@@ -2489,8 +2527,7 @@ async fn handle_child(
|
||||
panic::resume_unwind(p);
|
||||
}
|
||||
|
||||
(do_write, write_result) =
|
||||
tokio::spawn(append_logs(job_id, joined, db.clone())).remote_handle();
|
||||
(do_write, write_result) = tokio::spawn(append_logs(job_id, joined, db.clone())).remote_handle();
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!(%job_id, %err, "error reading output for job {job_id}: {err}");
|
||||
@@ -2500,6 +2537,7 @@ async fn handle_child(
|
||||
if *set_too_many_logs.borrow() {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* drop our end of the pipe */
|
||||
@@ -2813,7 +2851,7 @@ async fn handle_python_reqs(
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
let child = handle_child(&job.id, db, logs, child, false).await;
|
||||
let child = handle_child(&job.id, db, logs, child, false, worker_name).await;
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
|
||||
30
cli/apps.ts
30
cli/apps.ts
@@ -28,14 +28,18 @@ export class AppFile implements Resource, PushDiffs {
|
||||
remotePath: string,
|
||||
diffs: Difference[],
|
||||
): Promise<void> {
|
||||
if (await AppService.existsApp({ workspace, path: remotePath })) {
|
||||
let app: AppWithLastVersion | undefined = undefined;
|
||||
try {
|
||||
app = await AppService.getAppByPath({ workspace, path: remotePath });
|
||||
} catch (e) {}
|
||||
|
||||
if (app) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing app...`,
|
||||
`Applying ${diffs.length} diffs to existing app... ${remotePath}`,
|
||||
),
|
||||
);
|
||||
const changeset: {
|
||||
path?: string | undefined;
|
||||
summary?: string | undefined;
|
||||
value?: any;
|
||||
policy?: Policy | undefined;
|
||||
@@ -46,7 +50,7 @@ export class AppFile implements Resource, PushDiffs {
|
||||
(
|
||||
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
|
||||
diff.path.length !== 1 ||
|
||||
!["path", "summary"].includes(
|
||||
!["summary"].includes(
|
||||
diff.path[0] as string,
|
||||
)
|
||||
)
|
||||
@@ -61,6 +65,13 @@ export class AppFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
|
||||
if ((!changeset?.policy || JSON.stringify(changeset?.policy) == JSON.stringify(app.policy))
|
||||
&& (!changeset?.value || JSON.stringify(changeset?.value) == JSON.stringify(app.value))
|
||||
&& (!changeset?.summary || changeset.summary == app.summary)) {
|
||||
console.log(colors.yellow(`No changes to push for app ${remotePath}, skipping`))
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanges = Object.values(changeset).some((v) =>
|
||||
v !== null && typeof v !== "undefined"
|
||||
);
|
||||
@@ -87,19 +98,10 @@ export class AppFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: AppWithLastVersion | undefined;
|
||||
try {
|
||||
existing = await AppService.getAppByPath({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
12
cli/flow.ts
12
cli/flow.ts
@@ -94,6 +94,7 @@ export class FlowFile implements Resource, PushDiffs {
|
||||
...changeset,
|
||||
...base_changeset,
|
||||
}
|
||||
|
||||
await FlowService.updateFlow({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
@@ -114,20 +115,11 @@ export class FlowFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let remote: Flow | undefined;
|
||||
try {
|
||||
remote = await FlowService.getFlowByPath({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
|
||||
remote = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff(remote ?? {}, this, { cyclesFix: false }),
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
|
||||
import { colors, Command, FolderService, microdiff } from "./deps.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import {
|
||||
Difference,
|
||||
@@ -22,7 +22,9 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
owners: Array<string> | undefined;
|
||||
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
|
||||
extra_perms: Map<string, boolean> | undefined;
|
||||
|
||||
@property(() => String)
|
||||
display_name: string| undefined;
|
||||
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
if (remotePath.startsWith("/")) {
|
||||
remotePath = remotePath.substring(1);
|
||||
@@ -31,16 +33,10 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
remotePath = remotePath.substring(2);
|
||||
}
|
||||
|
||||
let existing: Folder | undefined;
|
||||
try {
|
||||
existing = await FolderService.getFolder({ workspace, name: remotePath });
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,22 +62,24 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
if (exists) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing folder...`,
|
||||
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`,
|
||||
),
|
||||
);
|
||||
|
||||
const changeset: {
|
||||
owners?: string[] | undefined;
|
||||
extra_perms?: any;
|
||||
display_name?: string | undefined;
|
||||
} = {};
|
||||
for (const diff of diffs) {
|
||||
if (
|
||||
diff.type !== "REMOVE" &&
|
||||
(
|
||||
diff.path.length !== 1 ||
|
||||
!["owners", "extra_perms"].includes(diff.path[0] as string)
|
||||
!["owners", "extra_perms", "display_name"].includes(diff.path[0] as string)
|
||||
)
|
||||
) {
|
||||
console.log(diff.path)
|
||||
throw new Error("Invalid folder diff with path " + diff.path);
|
||||
}
|
||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||
@@ -97,11 +95,10 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
await FolderService.updateFolder({
|
||||
workspace: workspace,
|
||||
name: remotePath,
|
||||
requestBody: changeset,
|
||||
requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined}
|
||||
});
|
||||
} else {
|
||||
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
|
||||
|
||||
@@ -13,7 +13,7 @@ import sync from "./sync.ts";
|
||||
import { tryResolveVersion } from "./context.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
|
||||
const VERSION = "v1.74.0";
|
||||
const VERSION = "v1.75.0";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
EditResourceType,
|
||||
microdiff,
|
||||
ResourceService,
|
||||
ResourceType,
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||
@@ -26,19 +25,10 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
description?: string;
|
||||
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: ResourceType | undefined;
|
||||
try {
|
||||
existing = await ResourceService.getResourceType({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
this.pushDiffs(
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,8 +55,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
colors.yellow(
|
||||
`Applying ${diffs.length} diffs to existing resource type...`,
|
||||
colors.yellow.bold(
|
||||
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
|
||||
),
|
||||
);
|
||||
const changeset: EditResourceType = {};
|
||||
|
||||
@@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
})
|
||||
) {
|
||||
console.log(
|
||||
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
|
||||
colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`),
|
||||
);
|
||||
|
||||
const changeset: EditResource = {
|
||||
@@ -51,11 +51,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
};
|
||||
for (const diff of diffs) {
|
||||
if (diff.path[0] === "is_oauth") {
|
||||
console.log(
|
||||
colors.yellow(
|
||||
"! is_oauth has been removed in newer versions. Ignoring.",
|
||||
),
|
||||
);
|
||||
//is_oauth is not updatable
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
@@ -64,9 +60,10 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
diff.path[0] !== "value" && (
|
||||
diff.path.length !== 1 ||
|
||||
diff.path[0] !== "description"
|
||||
)
|
||||
) && diff.path[0] !== "resource_type"
|
||||
)
|
||||
) {
|
||||
console.log(colors.red("Invalid variable diff with path " + diff.path));
|
||||
throw new Error("Invalid folder diff with path " + diff.path);
|
||||
}
|
||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||
@@ -110,19 +107,10 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: Resource | undefined;
|
||||
try {
|
||||
existing = await ResourceService.getResource({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export class ScriptFile {
|
||||
},
|
||||
toPlain: (data) => data,
|
||||
})
|
||||
@property(() => String)
|
||||
kind?: "script" | "failure" | "trigger" | "command" | "approval";
|
||||
|
||||
constructor(summary: string, description: string) {
|
||||
@@ -105,11 +106,21 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
} catch { }
|
||||
const language = inferContentTypeFromFilePath(path);
|
||||
|
||||
let remote = undefined
|
||||
try {
|
||||
const remote = await ScriptService.getScriptByPath({
|
||||
remote = await ScriptService.getScriptByPath({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch { }
|
||||
|
||||
if (remote) {
|
||||
if (typed.description === remote.description && content === remote.content && typed.summary === remote.summary && typed.is_template === remote.is_template && typed.kind == remote.kind && remote?.lock == typed.lock?.join('\n') && JSON.stringify(typed.schema) == JSON.stringify(remote.schema)) {
|
||||
console.log(colors.yellow(`No change to push for script ${remotePath}, skipping`))
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
await ScriptService.createScript({
|
||||
workspace,
|
||||
requestBody: {
|
||||
@@ -123,10 +134,11 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
lock: typed.lock,
|
||||
parent_hash: remote.hash,
|
||||
schema: typed.schema,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`))
|
||||
} catch {
|
||||
} else {
|
||||
// no parent hash
|
||||
await ScriptService.createScript({
|
||||
workspace: workspace,
|
||||
@@ -144,7 +156,6 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
},
|
||||
});
|
||||
console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`))
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
29
cli/sync.ts
29
cli/sync.ts
@@ -33,8 +33,8 @@ import { ResourceFile } from "./resource.ts";
|
||||
import { FlowFile } from "./flow.ts";
|
||||
import { VariableFile } from "./variable.ts";
|
||||
import { handleFile } from "./script.ts";
|
||||
import { equal } from "https://deno.land/x/equal/mod.ts";
|
||||
import { diffCharacters } from "https://deno.land/x/diff/mod.ts";
|
||||
import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts";
|
||||
import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts";
|
||||
type DynFSElement = {
|
||||
isDirectory: boolean;
|
||||
path: string;
|
||||
@@ -179,13 +179,13 @@ async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirecto
|
||||
return map;
|
||||
}
|
||||
async function compareDynFSElement(
|
||||
els1: DynFSElement, els2: DynFSElement,
|
||||
els1: DynFSElement, els2: DynFSElement | undefined,
|
||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||
raw: boolean
|
||||
): Promise<Change[]> {
|
||||
|
||||
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
|
||||
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]);
|
||||
const [m1, m2] = els2
|
||||
? await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)])
|
||||
: [await elementsToMap(els1, ignore), {}];
|
||||
|
||||
const changes: Change[] = [];
|
||||
|
||||
@@ -266,8 +266,8 @@ async function pull(
|
||||
|
||||
console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)"));
|
||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
|
||||
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
|
||||
const local = opts.raw ? undefined : await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
|
||||
const changes = await compareDynFSElement(remote, local, await ignoreF())
|
||||
|
||||
|
||||
console.log(`remote -> local: ${changes.length} changes to apply`);
|
||||
@@ -289,7 +289,7 @@ async function pull(
|
||||
|
||||
try {
|
||||
const currentLocal = await Deno.readTextFile(target)
|
||||
if (currentLocal !== change.before) {
|
||||
if (currentLocal !== change.before && currentLocal !== change.after) {
|
||||
console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`))
|
||||
if (opts.failConflicts) {
|
||||
conflicts.push({ local: currentLocal, change, path: change.path })
|
||||
@@ -459,7 +459,6 @@ function removeSuffix(str: string, suffix: string) {
|
||||
|
||||
async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) {
|
||||
|
||||
|
||||
if (!opts.raw) {
|
||||
if (!opts.skipPull) {
|
||||
console.log(colors.gray("You need to be up-to-date before pushing, pulling first."))
|
||||
@@ -474,9 +473,9 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
||||
|
||||
|
||||
console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)"));
|
||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
||||
const remote = opts.raw ? undefined : ZipFSElement((await downloadZip(workspace))!)
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), ""))
|
||||
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
|
||||
const changes = await compareDynFSElement(local, remote, await ignoreF())
|
||||
|
||||
console.log(`remote <- local: ${changes.length} changes to apply`);
|
||||
if (changes.length > 0) {
|
||||
@@ -575,7 +574,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Deno.remove(stateTarget)
|
||||
await Deno.remove(stateTarget)
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -616,7 +615,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull
|
||||
await file.pushDiffs(workspace, remotePath, diffs);
|
||||
} catch (e) {
|
||||
console.error("Failing to apply diffs to " + remotePath)
|
||||
console.error(e.body)
|
||||
console.error(JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,7 +634,7 @@ const command = new Command()
|
||||
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking.",
|
||||
)
|
||||
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
|
||||
.option("--skip-pull", "Push without pulling first")
|
||||
.option("--skip-pull", "Push without pulling first (you have pulled prior)")
|
||||
.option("--yes", "Push without needing confirmation")
|
||||
.option("--raw", "Push without using state, just overwrite.")
|
||||
.action(push as any);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
EditVariable,
|
||||
ListableVariable,
|
||||
microdiff,
|
||||
Table,
|
||||
VariableService,
|
||||
@@ -67,7 +66,7 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing variable...`,
|
||||
`Applying ${diffs.length} diffs to existing variable... ${remotePath}`,
|
||||
),
|
||||
);
|
||||
const changeset: EditVariable = {};
|
||||
@@ -76,11 +75,12 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
diff.type !== "REMOVE" &&
|
||||
(
|
||||
diff.path.length !== 1 ||
|
||||
!["path", "value", "is_secret", "description"].includes(
|
||||
!["path", "value", "is_secret", "description", "account", "is_oauth"].includes(
|
||||
diff.path[0] as string,
|
||||
)
|
||||
)
|
||||
) {
|
||||
console.log(colors.red("Invalid variable diff with path " + diff.path));
|
||||
throw new Error("Invalid variable diff with path " + diff.path);
|
||||
}
|
||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||
@@ -96,6 +96,7 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
await VariableService.updateVariable({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
@@ -103,7 +104,6 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
requestBody: changeset,
|
||||
});
|
||||
|
||||
console.log(changeset);
|
||||
} else {
|
||||
console.log(colors.yellow.bold("Creating new variable..."));
|
||||
await VariableService.createVariable({
|
||||
@@ -121,19 +121,10 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: ListableVariable | undefined;
|
||||
try {
|
||||
existing = await VariableService.getVariable({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill",
|
||||
"version": "1.74.0",
|
||||
"version": "1.75.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill",
|
||||
"version": "1.74.0",
|
||||
"version": "1.75.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
@@ -48,7 +48,6 @@
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelte-grid": "^5.1.6",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^5.1.14",
|
||||
@@ -1601,12 +1600,6 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@windmill-labs/svelte-grid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
|
||||
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@zeit/schemas": {
|
||||
"version": "2.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
|
||||
@@ -9284,12 +9277,6 @@
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@windmill-labs/svelte-grid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-grid/-/svelte-grid-5.1.6.tgz",
|
||||
"integrity": "sha512-wXHIG7XeMxwxnfaHajhjvz9c0u5uGgiQtTk/QxAVyViGdshsclfIfIWZF113QQBKCKp2jY/zP1cj64BrqioRoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@zeit/schemas": {
|
||||
"version": "2.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill",
|
||||
"version": "1.74.0",
|
||||
"version": "1.75.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -27,7 +27,6 @@
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelte-grid": "^5.1.6",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^5.1.14",
|
||||
|
||||
77
frontend/src/global.d.ts
vendored
77
frontend/src/global.d.ts
vendored
@@ -1,79 +1,2 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare type Item = import('svelte-dnd-action').Item
|
||||
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
|
||||
declare namespace svelte.JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@windmill-labs/svelte-grid' {
|
||||
import type { SvelteComponentTyped } from 'svelte'
|
||||
|
||||
export interface Size {
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export interface Positon {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface ItemLayout extends Size, Positon {
|
||||
fixed?: boolean
|
||||
resizable?: boolean
|
||||
draggable?: boolean
|
||||
customDragger?: boolean
|
||||
customResizer?: boolean
|
||||
min?: Size
|
||||
max?: Size
|
||||
}
|
||||
|
||||
export type Item<T> = T & { [width: number]: ItemLayout; data: any }
|
||||
export type FilledItem<T> = T & { [width: number]: Required<ItemLayout>; data: any }
|
||||
|
||||
export interface Props<T> {
|
||||
fillSpace?: boolean
|
||||
items: FilledItem<T>[]
|
||||
rowHeight: number
|
||||
cols: [number, number][]
|
||||
gap?: [number, number]
|
||||
fastStart?: boolean
|
||||
throttleUpdate?: number
|
||||
throttleResize?: number
|
||||
onTopId?: string
|
||||
scroller?: undefined
|
||||
sensor?: number
|
||||
parentWidth?: number
|
||||
}
|
||||
|
||||
export interface Slots<T> {
|
||||
default: { item: ItemLayout; dataItem: Item<T> }
|
||||
}
|
||||
|
||||
export default class Grid<T = {}> extends SvelteComponentTyped<
|
||||
Props<T>,
|
||||
{
|
||||
pointerup: CustomEvent<{ id: string }>
|
||||
mount: CustomEvent<>
|
||||
},
|
||||
Slots<T>
|
||||
> { }
|
||||
}
|
||||
|
||||
declare module '@windmill-labs/svelte-grid/build/helper/index.mjs' {
|
||||
import { ItemLayout } from '@windmill-labs/svelte-grid'
|
||||
|
||||
const x: {
|
||||
normalize(items: any[], col: any): unknown[]
|
||||
adjust(items: any[], col: any): unknown[]
|
||||
findSpace(item: any, items: any, cols: any): unknown
|
||||
|
||||
item<T>(obj: ItemLayout): Required<ItemLayout>
|
||||
}
|
||||
|
||||
export default x
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
$: key =
|
||||
apiTokenApps[resource_type]?.key ??
|
||||
(args != undefined
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key'].includes(x))[0]
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0]
|
||||
: undefined)
|
||||
|
||||
let scopes: string[] = []
|
||||
@@ -259,6 +259,7 @@
|
||||
args['token'] == '' &&
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '' &&
|
||||
args['key'] == '' &&
|
||||
key != undefined) ||
|
||||
(step == 3 && pathError != '') ||
|
||||
!isValid
|
||||
@@ -396,7 +397,7 @@
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
initialPath=""
|
||||
namePlaceholder="{resource_type}"
|
||||
namePlaceholder={resource_type}
|
||||
kind="resource"
|
||||
/>
|
||||
|
||||
@@ -433,7 +434,7 @@
|
||||
{:else}
|
||||
<Path
|
||||
initialPath=""
|
||||
namePlaceholder="{resource_type}"
|
||||
namePlaceholder={resource_type}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
kind="resource"
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import type { DropdownItem } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { MoreHorizontal } from 'lucide-svelte'
|
||||
import { MoreVertical } from 'lucide-svelte'
|
||||
import { Button, Menu } from './common'
|
||||
import { goto } from '$app/navigation'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
type Alignment = 'start' | 'end'
|
||||
type Side = 'top' | 'bottom'
|
||||
@@ -13,15 +14,24 @@
|
||||
export let dropdownItems: DropdownItem[]
|
||||
export let name: string | undefined = undefined
|
||||
export let placement: Placement = 'bottom-start'
|
||||
export let btnClasses = '!text-blue-500 bg-transparent'
|
||||
export let btnClasses = ''
|
||||
$: buttonClass = twMerge('!border-0 bg-transparent !p-[6px]', btnClasses)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Menu {placement} let:close>
|
||||
<Button nonCaptureEvent color="light" size="xs" {btnClasses} {...$$restProps} slot="trigger">
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
color="dark"
|
||||
variant="border"
|
||||
size="xs"
|
||||
btnClasses={buttonClass}
|
||||
{...$$restProps}
|
||||
slot="trigger"
|
||||
>
|
||||
{#if !$$slots.default}
|
||||
<MoreHorizontal size={20} />
|
||||
<MoreVertical size={20} />
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let name: string
|
||||
export let tabular = false
|
||||
export let order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources']
|
||||
|
||||
$: $workspaceStore && loadUsage()
|
||||
|
||||
@@ -13,10 +15,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col text-xs text-gray-600">
|
||||
{#each Object.entries(usage) as [k, v]}
|
||||
<div>
|
||||
{k}: {v}
|
||||
</div>
|
||||
{#if tabular}
|
||||
{#each order as key}
|
||||
<td class="text-center">
|
||||
{usage[key] ?? ''}
|
||||
</td>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col text-xs text-gray-600">
|
||||
{#each Object.entries(usage) as [k, v]}
|
||||
<div>
|
||||
{k}: {v}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -234,6 +234,14 @@
|
||||
name,
|
||||
requestBody: { username: member_name }
|
||||
})
|
||||
await GranularAclService.removeGranularAcls({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: name,
|
||||
kind: 'group_',
|
||||
requestBody: {
|
||||
owner: 'u/' + member_name
|
||||
}
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<span class="flex items-center space-x-2 mb-2">
|
||||
<h1>{title}</h1>
|
||||
{#if tooltip}
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
<Tooltip scale={0.9}>{tooltip}</Tooltip>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import FolderEditor from './FolderEditor.svelte'
|
||||
import GroupEditor from './GroupEditor.svelte'
|
||||
import { random_adj } from './random_positive_adjetive'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
let meta: Meta | undefined = undefined
|
||||
@@ -306,7 +307,7 @@
|
||||
{#if meta != undefined}
|
||||
<div class="flex gap-4 shrink">
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap"> </span>
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
|
||||
|
||||
<ToggleButtonGroup
|
||||
class="mt-0.5"
|
||||
@@ -409,8 +410,26 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-row flex justify-between w-full">
|
||||
<div><span class="font-mono text-sm break-all">{path}</span></div>
|
||||
<div class="flex flex-col w-full mt-4">
|
||||
<div class="flex justify-start w-full">
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-gray-300 !text-gray-600 !w-[70px] !h-[24px] rounded-r-none"
|
||||
>
|
||||
Full path
|
||||
</Badge>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={path}
|
||||
size={path?.length || 50}
|
||||
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
|
||||
on:focus={({ currentTarget }) => {
|
||||
currentTarget.select()
|
||||
}}
|
||||
/>
|
||||
<!-- <span class="font-mono text-sm break-all">{path}</span> -->
|
||||
</div>
|
||||
<div class="text-red-600 text-2xs">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
on:refresh={async (e) => {
|
||||
await loadResources(resourceType)
|
||||
value = e.detail
|
||||
valueSelect = { value: e.detail, label: e.detail }
|
||||
}}
|
||||
newPageOAuth
|
||||
bind:this={appConnect}
|
||||
@@ -87,7 +88,7 @@
|
||||
}}
|
||||
items={collection}
|
||||
class="text-clip grow min-w-0"
|
||||
placeholder="{resourceType} resource"
|
||||
placeholder="{resourceType ?? 'any'} resource"
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles}
|
||||
/>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
label: 'Failed',
|
||||
data:
|
||||
failed?.map((job) => ({
|
||||
x: job.created_at as any,
|
||||
x: job.started_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
@@ -57,7 +57,7 @@
|
||||
label: 'Successful',
|
||||
data:
|
||||
success?.map((job) => ({
|
||||
x: job.created_at as any,
|
||||
x: job.started_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
@@ -71,7 +71,10 @@
|
||||
enabled: true,
|
||||
modifierKey: 'ctrl' as 'ctrl',
|
||||
onPanComplete: ({ chart }) => {
|
||||
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
|
||||
dispatch('zoom', {
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
@@ -80,7 +83,10 @@
|
||||
},
|
||||
mode: 'x' as 'x',
|
||||
onZoom: ({ chart }) => {
|
||||
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
|
||||
dispatch('zoom', {
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +94,22 @@
|
||||
function getPath(x: any): string {
|
||||
return x.path
|
||||
}
|
||||
|
||||
$: minTime = getMinTime(jobs)
|
||||
|
||||
function addSeconds(date: Date, seconds: number): Date {
|
||||
date.setTime(date.getTime() + seconds * 1000)
|
||||
return date
|
||||
}
|
||||
function getMinTime(jobs: CompletedJob[] | undefined): Date {
|
||||
return addSeconds(new Date(jobs?.[jobs?.length - 1]?.started_at ?? new Date().toString()), -15)
|
||||
}
|
||||
|
||||
$: maxTime = getMaxTime(jobs)
|
||||
|
||||
function getMaxTime(jobs: CompletedJob[] | undefined): Date {
|
||||
return addSeconds(new Date(jobs?.[0]?.started_at ?? new Date().toString()), 15)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scatter
|
||||
@@ -115,7 +137,8 @@
|
||||
display: false
|
||||
},
|
||||
type: 'time',
|
||||
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
|
||||
min: minTime,
|
||||
max: maxTime
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Script, ScriptService } from '$lib/gen'
|
||||
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode, isInitialCode } from '$lib/script_helpers'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import ScriptEditor from './ScriptEditor.svelte'
|
||||
import ScriptSchema from './ScriptSchema.svelte'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
|
||||
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
|
||||
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import SettingSection from './SettingSection.svelte'
|
||||
|
||||
export let script: Script
|
||||
export let initialPath: string = ''
|
||||
@@ -31,14 +31,34 @@
|
||||
['Go', Script.language.GO],
|
||||
['Bash', Script.language.BASH]
|
||||
]
|
||||
let viewScriptKind = script.kind !== Script.kind.SCRIPT
|
||||
const scriptKindOptions: { value: Script.kind; title: string; desc?: string }[] = [
|
||||
{
|
||||
value: Script.kind.SCRIPT,
|
||||
title: 'Action'
|
||||
},
|
||||
{
|
||||
value: Script.kind.TRIGGER,
|
||||
title: 'Trigger',
|
||||
desc: 'First module of flows to trigger them based on external changes. These kind of scripts are usually running on a schedule to periodically look for changes.'
|
||||
},
|
||||
{
|
||||
value: Script.kind.APPROVAL,
|
||||
title: 'Approval',
|
||||
desc: 'Send notifications externally to ask for approval to continue a flow.'
|
||||
},
|
||||
{
|
||||
value: Script.kind.FAILURE,
|
||||
title: 'Error Handler',
|
||||
desc: 'Handle errors in flows after all retry attempts have been exhausted.'
|
||||
}
|
||||
]
|
||||
|
||||
let pathError = ''
|
||||
|
||||
let summaryC: HTMLInputElement | undefined = undefined
|
||||
let pathC: Path | undefined = undefined
|
||||
let loadingSave = false
|
||||
|
||||
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
|
||||
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
|
||||
$: step = Number($page.url.searchParams.get('step')) || 1
|
||||
|
||||
if (script.content == '') {
|
||||
@@ -53,7 +73,6 @@
|
||||
script.content = initialCode(language, kind, template)
|
||||
}
|
||||
|
||||
let loadingSave = false
|
||||
async function editScript(leave: boolean): Promise<void> {
|
||||
loadingSave = true
|
||||
try {
|
||||
@@ -250,7 +269,8 @@
|
||||
}}
|
||||
disabled={lockedLanguage}
|
||||
>
|
||||
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</span>
|
||||
<LanguageIcon {lang} />
|
||||
<span class="ml-2 py-2">{label}</span>
|
||||
</Button>
|
||||
{/each}
|
||||
<Button
|
||||
@@ -265,7 +285,7 @@
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">PostgreSQL</span>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
|
||||
</Button>
|
||||
<!-- <Button
|
||||
size="sm"
|
||||
@@ -278,65 +298,41 @@
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2 py-4">MySQL</span>
|
||||
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
|
||||
</Button> -->
|
||||
</div>
|
||||
<div class="mt-16 mb-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewScriptKind = !viewScriptKind)}
|
||||
>
|
||||
Tag this script as having a specific purpose inside flows
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewScriptKind}
|
||||
<div class="max-w-lg">
|
||||
<RadioButton
|
||||
label="Script Type"
|
||||
options={[
|
||||
['Action', Script.kind.SCRIPT],
|
||||
[
|
||||
{
|
||||
title: 'Trigger',
|
||||
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
|
||||
},
|
||||
Script.kind.TRIGGER
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Approval',
|
||||
desc: `Send notification externally to ask for approval to continue a flow`
|
||||
},
|
||||
Script.kind.APPROVAL
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Error Handler',
|
||||
desc: `Handle errors for flows after all retries attempts have been exhausted`
|
||||
},
|
||||
Script.kind.FAILURE
|
||||
]
|
||||
|
||||
// ['Command Handler', Script.kind.COMMAND]
|
||||
]}
|
||||
on:change={(e) => {
|
||||
if (isInitialCode(script.content)) {
|
||||
<SettingSection
|
||||
title="Script kind"
|
||||
element="h3"
|
||||
tooltip="Tag this script as having a specific purpose inside flows. If it won't be used in flows,
|
||||
you don't have to worry about this."
|
||||
accordion
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each scriptKindOptions as { value, title, desc }}
|
||||
{@const isPicked = script.kind === value}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={isPicked ? 'blue' : 'dark'}
|
||||
btnClasses="font-medium {isPicked ? '!bg-blue-50/75' : ''}"
|
||||
on:click={() => {
|
||||
template = 'script'
|
||||
initContent(script.language, e.detail, template)
|
||||
}
|
||||
}}
|
||||
bind:value={script.kind}
|
||||
/>
|
||||
script.kind = value
|
||||
initContent(script.language, value, template)
|
||||
}}
|
||||
disabled={lockedLanguage}
|
||||
>
|
||||
{title}
|
||||
{#if desc}
|
||||
<Tooltip class="mb-0.5 ml-1">
|
||||
{desc}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- <div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
</div> -->
|
||||
</SettingSection>
|
||||
</CenteredPage>
|
||||
{:else if step === 2}
|
||||
<ScriptEditor
|
||||
|
||||
43
frontend/src/lib/components/SettingSection.svelte
Normal file
43
frontend/src/lib/components/SettingSection.svelte
Normal file
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import type { IntRange } from '../common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let title: string
|
||||
export let tooltip: string
|
||||
export let element: `h${IntRange<1, 6>}` = 'h2'
|
||||
export let accordion = false
|
||||
let showContent = !accordion
|
||||
</script>
|
||||
|
||||
<div class="border-b [&:has(button:hover)]:border-gray-400 duration-200 pb-1 mt-8 mb-2">
|
||||
{#if accordion}
|
||||
<button
|
||||
class="flex w-full justify-between items-center"
|
||||
on:click={() => (showContent = !showContent)}
|
||||
>
|
||||
<svelte:element this={element}>
|
||||
{title}
|
||||
<Tooltip scale={0.9} class="mb-0.5">
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
</svelte:element>
|
||||
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1">
|
||||
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!rotate-180' : ''}" />
|
||||
</span>
|
||||
</button>
|
||||
{:else}
|
||||
<svelte:element this={element}>
|
||||
{title}
|
||||
<Tooltip scale={0.9} class="mb-0.5">
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
</svelte:element>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showContent}
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2,13 +2,18 @@
|
||||
import { ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let text: string
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let view = false
|
||||
</script>
|
||||
|
||||
<Button color="light" on:click={() => (view = !view)} variant="border"
|
||||
>{text}
|
||||
{#if tooltip}
|
||||
<Tooltip wrapperClass="mx-1">{tooltip}</Tooltip>
|
||||
{/if}
|
||||
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
|
||||
>
|
||||
{#if view}
|
||||
|
||||
@@ -186,9 +186,9 @@
|
||||
syncIteration++
|
||||
await loadTestJob(id)
|
||||
let nextIteration = 50
|
||||
if (syncIteration == ITERATIONS_BEFORE_SLOW_REFRESH) {
|
||||
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
|
||||
nextIteration = 500
|
||||
} else if (syncIteration == ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
|
||||
} else if (syncIteration > ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
|
||||
nextIteration = 2000
|
||||
}
|
||||
setTimeout(() => syncer(id), nextIteration)
|
||||
|
||||
@@ -18,5 +18,7 @@
|
||||
data={faInfoCircle}
|
||||
{scale}
|
||||
/>
|
||||
<span slot="text"> <slot /> </span>
|
||||
<svelte:fragment slot="text">
|
||||
<slot />
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
const { runnableComponents, worldStore, app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { runnableComponents, worldStore, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let labelValue: string
|
||||
let color: ButtonType.Color
|
||||
@@ -127,6 +127,7 @@
|
||||
<InputValue
|
||||
{id}
|
||||
input={configuration.disabled}
|
||||
extraContext={extraQueryParams}
|
||||
bind:value={disabled}
|
||||
bind:error={errors.disabled}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -24,7 +24,7 @@
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
const { app, runnableComponents, worldStore, stateId } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let labelValue: string = 'Default label'
|
||||
let color: ButtonType.Color
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
|
||||
const { app, runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, runnableComponents, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let labelValue: string = 'Default label'
|
||||
let color: ButtonType.Color
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -27,7 +27,7 @@
|
||||
export let render: boolean
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { AppInput } from '../../inputType'
|
||||
import {
|
||||
IS_APP_PUBLIC_CONTEXT_KEY,
|
||||
type AppEditorContext,
|
||||
type AppViewerContext,
|
||||
type ComponentCustomCSS
|
||||
} from '../../types'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
@@ -17,7 +17,7 @@
|
||||
export let render: boolean
|
||||
|
||||
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let result: any = undefined
|
||||
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
export let render: boolean
|
||||
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let result: string | undefined = undefined
|
||||
let h: number | undefined = undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import { AlignWrapper, InputValue } from '../helpers'
|
||||
import { loadIcon } from '../icon'
|
||||
@@ -14,7 +14,7 @@
|
||||
export let customCss: ComponentCustomCSS<'container' | 'icon'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let icon: string | undefined = undefined
|
||||
let size: number
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { staticValues } from '../../editor/componentsPanel/componentStaticValues'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
export let customCss: ComponentCustomCSS<'image'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const fit: Record<FitOption, string> = {
|
||||
cover: 'object-cover',
|
||||
contain: 'object-contain',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { InputValue } from '../helpers'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Map, View, Feature } from 'ol'
|
||||
@@ -35,7 +35,7 @@
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, selectedComponent, connectingInput, focusedGrid, mode } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
mapRegion: Output<{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { getDocument, type PDFDocumentProxy, type PDFPageProxy } from 'pdfjs-dist'
|
||||
import 'pdfjs-dist/build/pdf.worker.entry'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { throttle } from '../../../../utils'
|
||||
@@ -19,7 +19,7 @@
|
||||
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, mode, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, mode, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let source: string | ArrayBuffer | undefined = undefined
|
||||
let wrapper: HTMLDivElement | undefined = undefined
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
export let render: boolean
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import type { ChartOptions, ChartData } from 'chart.js'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -32,7 +32,7 @@
|
||||
let pannable = false
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { getContext } from 'svelte'
|
||||
import ResizeWrapper from '../helpers/ResizeWrapper.svelte'
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
export const staticOutputs: string[] = ['result', 'loading']
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let result: string | undefined = undefined
|
||||
let style: 'Title' | 'Subtitle' | 'Body' | 'Caption' | 'Label' | undefined = undefined
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { Scatter } from 'svelte-chartjs'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import type { ChartOptions, ChartData } from 'chart.js'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { getContext } from 'svelte'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
export let render: boolean
|
||||
|
||||
export const staticOutputs: string[] = ['loading', 'result']
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let logarithmicScale = false
|
||||
let zoomable = false
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { Output } from '../../../rx'
|
||||
import type { AppEditorContext } from '../../../types'
|
||||
import type { AppViewerContext } from '../../../types'
|
||||
import InputValue from '../../helpers/InputValue.svelte'
|
||||
import type { AppInput } from '../../../inputType'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
let result: Record<string, any>[] | undefined = undefined
|
||||
|
||||
const { worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let selectedRowIndex = -1
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { Output } from '../../../rx'
|
||||
import type { AppEditorContext, BaseAppComponent, ComponentCustomCSS } from '../../../types'
|
||||
import type { AppViewerContext, BaseAppComponent, ComponentCustomCSS } from '../../../types'
|
||||
import InputValue from '../../helpers/InputValue.svelte'
|
||||
import type { AppInput } from '../../../inputType'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
@@ -61,7 +61,7 @@
|
||||
app,
|
||||
worldStore,
|
||||
staticOutputs: staticOutputsStore
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let selectedRowIndex = -1
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { accessPropertyByPath } from '../../utils'
|
||||
|
||||
type T = string | number | boolean | Record<string | number, any> | undefined
|
||||
@@ -12,6 +12,7 @@
|
||||
export let value: T
|
||||
export let id: string | undefined = undefined
|
||||
export let error: string = ''
|
||||
export let extraContext: Record<string, any> = {}
|
||||
|
||||
let lastInput = input ? JSON.parse(JSON.stringify(input)) : undefined
|
||||
|
||||
@@ -23,7 +24,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: state = $worldStore?.state
|
||||
|
||||
@@ -72,16 +73,19 @@
|
||||
}
|
||||
|
||||
function computeGlobalContext() {
|
||||
return Object.fromEntries(
|
||||
Object.entries($worldStore?.outputsById ?? {})
|
||||
.filter(([k, _]) => k != id)
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
|
||||
]
|
||||
})
|
||||
)
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries($worldStore?.outputsById ?? {})
|
||||
.filter(([k, _]) => k != id)
|
||||
.map(([key, value]) => {
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(value ?? {}).map((x) => [x[0], x[1].peak()]))
|
||||
]
|
||||
})
|
||||
),
|
||||
...extraContext
|
||||
}
|
||||
}
|
||||
|
||||
export function getValue(input: AppInput) {
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import InputValue from './InputValue.svelte'
|
||||
|
||||
export let componentInput: AppInput
|
||||
export let id: string
|
||||
export let result: any
|
||||
export let render: boolean
|
||||
|
||||
// Sync the result to the output
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
loading: Output<boolean>
|
||||
@@ -32,4 +35,16 @@
|
||||
<InputValue {id} input={componentInput} bind:value={result} />
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
{#if render}
|
||||
<slot />
|
||||
{:else}
|
||||
<div class="w-full h-full">
|
||||
<div
|
||||
out:fade|local={{ duration: 50 }}
|
||||
class="absolute inset-0 center-center flex-col bg-white text-gray-600 border"
|
||||
>
|
||||
<Loader2 class="animate-spin" size={16} />
|
||||
<span class="text-xs mt-1">Loading</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
|
||||
import { RefreshCcw, RefreshCw } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
|
||||
export let componentId: string
|
||||
|
||||
const { runnableComponents, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { runnableComponents, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
async function refresh() {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { findGridItem } from '../../editor/appUtils'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
|
||||
export let id: string
|
||||
export let shouldWrap: boolean = false
|
||||
const { app, breakpoint } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, breakpoint } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: gridItem = findGridItem($app, id)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { AppInputs, Runnable } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import InputValue from './InputValue.svelte'
|
||||
import RefreshButton from './RefreshButton.svelte'
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
errorByComponent,
|
||||
mode,
|
||||
stateId
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
onMount(() => {
|
||||
if (autoRefresh) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import { isScriptByNameDefined, isScriptByPathDefined } from '../../utils'
|
||||
import NonRunnableComponent from './NonRunnableComponent.svelte'
|
||||
import RunnableComponent from './RunnableComponent.svelte'
|
||||
@@ -24,7 +22,7 @@
|
||||
export let gotoNewTab: boolean | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { staticExporter, noBackend } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { staticExporter, noBackend } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: if (initializing && result) {
|
||||
initializing = false
|
||||
@@ -64,18 +62,8 @@
|
||||
>
|
||||
<slot />
|
||||
</RunnableComponent>
|
||||
{:else if render}
|
||||
<NonRunnableComponent bind:result {id} {componentInput}>
|
||||
{:else}
|
||||
<NonRunnableComponent {render} bind:result {id} {componentInput}>
|
||||
<slot />
|
||||
</NonRunnableComponent>
|
||||
{:else if !render}
|
||||
<div class="w-full h-full">
|
||||
<div
|
||||
out:fade|local={{ duration: 50 }}
|
||||
class="absolute inset-0 center-center flex-col bg-white text-gray-600 border"
|
||||
>
|
||||
<Loader2 class="animate-spin" size={16} />
|
||||
<span class="text-xs mt-1">Loading</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -15,7 +15,7 @@
|
||||
export let customCss: ComponentCustomCSS<'text'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
export const staticOutputs: string[] = ['result']
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -16,7 +16,7 @@
|
||||
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let labelValue: string = 'Title'
|
||||
let minValue: string = ''
|
||||
let maxValue: string = ''
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { FileInput } from '../../../common'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let acceptedFileTypes: string[] | undefined = undefined
|
||||
let allowMultiple: boolean | undefined = undefined
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Select from 'svelte-select'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -18,7 +18,7 @@
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, connectingInput, selectedComponent } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: { label: string; value: string }[]
|
||||
let placeholder: string = 'Select an item'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -15,7 +15,7 @@
|
||||
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let defaultValue: number | undefined = undefined
|
||||
let placeholder: string | undefined = undefined
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
@@ -17,7 +17,7 @@
|
||||
undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let min = 0
|
||||
let max = 42
|
||||
let step = 1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Select from 'svelte-select'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -18,7 +18,7 @@
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, connectingInput, selectedComponent } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: { label: string; value: any; created?: boolean }[]
|
||||
let placeholder: string = 'Select an item'
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
: []
|
||||
if (defaultValue) {
|
||||
value = JSON.stringify(defaultValue)
|
||||
} else if (listItems.length > 0) {
|
||||
value = listItems[0]?.value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -17,7 +17,7 @@
|
||||
undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let min = 0
|
||||
let max = 42
|
||||
let step = 1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
@@ -18,7 +18,7 @@
|
||||
'textinputcomponent'
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let placeholder: string | undefined = undefined
|
||||
let defaultValue: string | undefined = undefined
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppInput } from '../../../inputType'
|
||||
import type { Output } from '../../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../../types'
|
||||
import { concatCustomCss } from '../../../utils'
|
||||
import AlignWrapper from '../../helpers/AlignWrapper.svelte'
|
||||
import InputValue from '../../helpers/InputValue.svelte'
|
||||
@@ -16,7 +16,7 @@
|
||||
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let defaultValue: number | undefined = undefined
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS, GridItem } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS, GridItem } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
let noPadding: boolean | undefined = undefined
|
||||
|
||||
export const staticOutputs: string[] = []
|
||||
const { app, focusedGrid, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function onFocus() {
|
||||
$focusedGrid = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type {
|
||||
AppEditorContext,
|
||||
AppViewerContext,
|
||||
ComponentCustomCSS,
|
||||
HorizontalAlignment,
|
||||
VerticalAlignment
|
||||
@@ -20,7 +20,7 @@
|
||||
export let position: 'horizontal' | 'vertical'
|
||||
export let render: boolean
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let size = 2
|
||||
let color = '#00000060'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import Portal from 'svelte-portal'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
@@ -18,7 +18,7 @@
|
||||
export let noWFull = false
|
||||
export let render: boolean
|
||||
|
||||
const { app, focusedGrid, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let gridContent: string[] | undefined = undefined
|
||||
let noPadding: boolean | undefined = undefined
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppEditorContext, AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
@@ -16,8 +16,9 @@
|
||||
export let panes: number[]
|
||||
export let render: boolean
|
||||
|
||||
const { app, focusedGrid, selectedComponent, componentControl } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let noPadding: boolean | undefined = undefined
|
||||
|
||||
@@ -43,10 +44,12 @@
|
||||
return false
|
||||
},
|
||||
right: () => {
|
||||
if ($focusedGrid?.subGridIndex) {
|
||||
// subGridIndex can be 0
|
||||
if ($focusedGrid?.subGridIndex !== undefined) {
|
||||
const index = $focusedGrid?.subGridIndex ?? 0
|
||||
|
||||
if (index < panes.length - 1) {
|
||||
$focusedGrid.subGridIndex = index - 1
|
||||
$focusedGrid.subGridIndex = index + 1
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Tab, Tabs } from '$lib/components/common'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { Output } from '../../rx'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import type { AppEditorContext, AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
|
||||
@@ -18,13 +19,21 @@
|
||||
export let render: boolean
|
||||
|
||||
export const staticOutputs: string[] = ['selectedTabIndex']
|
||||
const { app, worldStore, focusedGrid, selectedComponent, componentControl } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, worldStore, focusedGrid, selectedComponent } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let selected: string = tabs[0]
|
||||
let noPadding: boolean | undefined = undefined
|
||||
let tabHeight: number = 0
|
||||
|
||||
$: outputs = $worldStore
|
||||
? initOutput($worldStore, id, {
|
||||
selectedTabIndex: 0
|
||||
})
|
||||
: undefined
|
||||
|
||||
function handleTabSelection() {
|
||||
const selectedIndex = tabs?.indexOf(selected)
|
||||
outputs?.selectedTabIndex.set(selectedIndex, true)
|
||||
@@ -74,10 +83,6 @@
|
||||
|
||||
$: selected && handleTabSelection()
|
||||
|
||||
$: outputs = $worldStore?.outputsById[id] as {
|
||||
selectedTabIndex: Output<number>
|
||||
}
|
||||
|
||||
$: selectedIndex = tabs?.indexOf(selected) ?? -1
|
||||
|
||||
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
|
||||
@@ -86,7 +91,7 @@
|
||||
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
|
||||
|
||||
<div>
|
||||
<div bind:clientHeight={tabHeight} on:pointerdown|stopPropagation>
|
||||
<div bind:clientHeight={tabHeight}>
|
||||
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
|
||||
{#each tabs ?? [] as res}
|
||||
<Tab
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { buildWorld, type World } from '../rx'
|
||||
import type {
|
||||
App,
|
||||
AppEditorContext,
|
||||
AppViewerContext,
|
||||
ConnectingInput,
|
||||
EditorBreakpoint,
|
||||
EditorMode,
|
||||
@@ -32,11 +33,13 @@
|
||||
|
||||
import SettingsPanel from './SettingsPanel.svelte'
|
||||
import { fly } from 'svelte/transition'
|
||||
import type { Policy } from '$lib/gen'
|
||||
import { VariableService, type Policy } from '$lib/gen'
|
||||
import { page } from '$app/stores'
|
||||
import CssSettings from './componentsPanel/CssSettings.svelte'
|
||||
import { initHistory } from '$lib/history'
|
||||
import ComponentNavigation from './component/ComponentNavigation.svelte'
|
||||
import ItemPicker from '$lib/components/ItemPicker.svelte'
|
||||
import VariableEditor from '$lib/components/VariableEditor.svelte'
|
||||
|
||||
export let app: App
|
||||
export let path: string
|
||||
@@ -62,8 +65,9 @@
|
||||
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
|
||||
const errorByComponent = writable<Record<string, { error: string; componentId: string }>>({})
|
||||
const focusedGrid = writable<FocusedGrid | undefined>(undefined)
|
||||
const pickVariableCallback: Writable<((path: string) => void) | undefined> = writable(undefined)
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
setContext<AppViewerContext>('AppViewerContext', {
|
||||
worldStore,
|
||||
staticOutputs,
|
||||
app: appStore,
|
||||
@@ -84,9 +88,13 @@
|
||||
openDebugRun: writable(undefined),
|
||||
focusedGrid,
|
||||
stateId: writable(0),
|
||||
parentWidth: writable(0),
|
||||
parentWidth: writable(0)
|
||||
})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
history,
|
||||
componentControl: writable({})
|
||||
componentControl: writable({}),
|
||||
pickVariableCallback
|
||||
})
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -132,6 +140,14 @@
|
||||
} else {
|
||||
selectedTab = 'insert'
|
||||
}
|
||||
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
|
||||
$: if ($pickVariableCallback) {
|
||||
itemPicker?.openDrawer()
|
||||
}
|
||||
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
</script>
|
||||
|
||||
<svelte:window on:hashchange={hashchange} />
|
||||
@@ -142,145 +158,182 @@
|
||||
/>
|
||||
{/if}
|
||||
{#if !$userStore?.operator}
|
||||
{#if initialMode !== 'preview'}
|
||||
<AppEditorHeader {policy} {fromHub} />
|
||||
{/if}
|
||||
{#if $appStore}
|
||||
{#if initialMode !== 'preview'}
|
||||
<AppEditorHeader {policy} {fromHub} />
|
||||
{/if}
|
||||
|
||||
{#if previewing}
|
||||
<SplitPanesWrapper>
|
||||
<div
|
||||
class={twMerge('h-full w-full', $appStore.css?.['app']?.['viewer']?.class)}
|
||||
style={$appStore.css?.['app']?.['viewer']?.style}
|
||||
>
|
||||
<AppPreview
|
||||
workspace={$workspaceStore ?? ''}
|
||||
summary={$summaryStore}
|
||||
app={$appStore}
|
||||
appPath={path}
|
||||
{breakpoint}
|
||||
{policy}
|
||||
isEditor
|
||||
{context}
|
||||
noBackend={false}
|
||||
/>
|
||||
</div>
|
||||
</SplitPanesWrapper>
|
||||
{:else}
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes class="max-w-full overflow-hidden">
|
||||
<Pane size={$connectingInput?.opened ? 40 : 15} minSize={5} maxSize={33}>
|
||||
<ContextPanel />
|
||||
</Pane>
|
||||
<Pane size={64}>
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={$connectingInput?.opened ? 100 : 70}>
|
||||
<div
|
||||
on:pointerdown={(e) => {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
class={twMerge(
|
||||
'bg-gray-100 h-full w-full',
|
||||
$appStore.css?.['app']?.['viewer']?.class
|
||||
)}
|
||||
style={$appStore.css?.['app']?.['viewer']?.style}
|
||||
>
|
||||
{#if previewing}
|
||||
<SplitPanesWrapper>
|
||||
<div
|
||||
class={twMerge('h-full w-full', $appStore.css?.['app']?.['viewer']?.class)}
|
||||
style={$appStore.css?.['app']?.['viewer']?.style}
|
||||
>
|
||||
<AppPreview
|
||||
workspace={$workspaceStore ?? ''}
|
||||
summary={$summaryStore}
|
||||
app={$appStore}
|
||||
appPath={path}
|
||||
{breakpoint}
|
||||
{policy}
|
||||
isEditor
|
||||
{context}
|
||||
noBackend={false}
|
||||
/>
|
||||
</div>
|
||||
</SplitPanesWrapper>
|
||||
{:else}
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes class="max-w-full overflow-hidden">
|
||||
<Pane size={15} minSize={5} maxSize={33}>
|
||||
<ContextPanel />
|
||||
</Pane>
|
||||
<Pane size={64}>
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={70}>
|
||||
<div
|
||||
class={classNames(
|
||||
'relative mx-auto w-full h-full overflow-auto',
|
||||
app.fullscreen ? '' : 'max-w-6xl'
|
||||
on:pointerdown={(e) => {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
class={twMerge(
|
||||
'bg-gray-100 h-full w-full',
|
||||
$appStore.css?.['app']?.['viewer']?.class
|
||||
)}
|
||||
style={$appStore.css?.['app']?.['viewer']?.style}
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
<ComponentNavigation />
|
||||
<div
|
||||
class={classNames(
|
||||
'relative mx-auto w-full h-full overflow-auto',
|
||||
app.fullscreen ? '' : 'max-w-6xl'
|
||||
)}
|
||||
>
|
||||
{#if $appStore.grid}
|
||||
<ComponentNavigation />
|
||||
|
||||
<div on:pointerdown|stopPropagation class={width}>
|
||||
<GridEditor {policy} />
|
||||
<div on:pointerdown|stopPropagation class={width}>
|
||||
<GridEditor {policy} />
|
||||
</div>
|
||||
|
||||
<div id="app-editor-top-level-drawer" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={$connectingInput?.opened ? 0 : 30}>
|
||||
<div class="relative h-full w-full">
|
||||
<InlineScriptsPanel />
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</Pane>
|
||||
<Pane size={21} minSize={5} maxSize={33}>
|
||||
<div class="relative flex flex-col h-full">
|
||||
<Tabs bind:selected={selectedTab} wrapperClass="!h-[40px]" class="!h-full">
|
||||
<Tab value="insert" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faPlus} />
|
||||
<span>Insert</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="settings" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faSliders} />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="css" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faCode} />
|
||||
<span>CSS</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<div slot="content" class="h-full overflow-y-auto pb-4">
|
||||
<TabContent class="overflow-auto h-full" value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
<SettingsPanel />
|
||||
{:else}
|
||||
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
|
||||
Select a component to see the settings for it
|
||||
</div>
|
||||
|
||||
<div id="app-editor-top-level-drawer" />
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="insert">
|
||||
<ComponentList />
|
||||
</TabContent>
|
||||
<TabContent value="css">
|
||||
<CssSettings />
|
||||
</TabContent>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={$connectingInput?.opened ? 0 : 30}>
|
||||
<div class="relative h-full w-full">
|
||||
<InlineScriptsPanel />
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</Pane>
|
||||
<Pane size={21} minSize={5} maxSize={33}>
|
||||
<div class="relative flex flex-col h-full">
|
||||
<Tabs bind:selected={selectedTab} wrapperClass="!h-[40px]" class="!h-full">
|
||||
<Tab value="insert" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faPlus} />
|
||||
<span>Insert</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="settings" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faSliders} />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab value="css" size="xs">
|
||||
<div class="m-1 center-center gap-2">
|
||||
<Icon data={faCode} />
|
||||
<span>CSS</span>
|
||||
</div>
|
||||
</Tab>
|
||||
<div slot="content" class="h-full overflow-y-auto pb-4">
|
||||
<TabContent class="overflow-auto" value="settings">
|
||||
{#if $selectedComponent !== undefined}
|
||||
<SettingsPanel />
|
||||
{:else}
|
||||
<div class="min-w-[150px] text-sm text-gray-500 text-center py-8 px-2">
|
||||
Select a component to see the settings for it
|
||||
</Tabs>
|
||||
{#if $connectingInput.opened}
|
||||
<div
|
||||
class="fixed top-32 p-2 z-50 flex justify-center items-center"
|
||||
transition:fly|local={{ duration: 100, y: -100 }}
|
||||
>
|
||||
<Alert title="Connecting" type="info">
|
||||
<div class="flex gap-2 flex-col">
|
||||
Click on the output of the component you want to connect to on the left panel.
|
||||
<div>
|
||||
<Button
|
||||
color="blue"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
$connectingInput.opened = false
|
||||
$connectingInput.input = undefined
|
||||
}}
|
||||
>
|
||||
Stop connecting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value="insert">
|
||||
<ComponentList />
|
||||
</TabContent>
|
||||
<TabContent value="css">
|
||||
<CssSettings />
|
||||
</TabContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
{#if $connectingInput.opened}
|
||||
<div
|
||||
class="fixed top-32 p-2 z-50 flex justify-center items-center"
|
||||
transition:fly|local={{ duration: 100, y: -100 }}
|
||||
>
|
||||
<Alert title="Connecting" type="info">
|
||||
<div class="flex gap-2 flex-col">
|
||||
Click on the output of the component you want to connect to on the left panel.
|
||||
<div>
|
||||
<Button
|
||||
color="blue"
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
$connectingInput.opened = false
|
||||
$connectingInput.input = undefined
|
||||
}}
|
||||
>
|
||||
Stop connecting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</SplitPanesWrapper>
|
||||
{/if}
|
||||
{:else}
|
||||
App is null
|
||||
{/if}
|
||||
{:else}
|
||||
App editor not available to operators
|
||||
{/if}
|
||||
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, _) => {
|
||||
$pickVariableCallback?.(path)
|
||||
}}
|
||||
itemName="Variable"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
...x
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
slot="submission"
|
||||
class="flex flex-row-reverse w-full bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
|
||||
>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
variableEditor?.initNew?.()
|
||||
}}
|
||||
>
|
||||
New variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
|
||||
import { idMutex } from '$lib/components/flows/flowStateUtils'
|
||||
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
|
||||
import JobArgs from '$lib/components/JobArgs.svelte'
|
||||
import LogViewer from '$lib/components/LogViewer.svelte'
|
||||
@@ -27,7 +28,7 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { AppService, Job, Policy } from '$lib/gen'
|
||||
import { redo, undo } from '$lib/history'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
faBug,
|
||||
faClipboard,
|
||||
@@ -61,7 +62,7 @@
|
||||
StaticAppInput,
|
||||
UserAppInput
|
||||
} from '../inputType'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import { allItems, toStatic } from '../utils'
|
||||
import AppExportButton from './AppExportButton.svelte'
|
||||
import AppInputs from './AppInputs.svelte'
|
||||
@@ -90,9 +91,10 @@
|
||||
errorByComponent,
|
||||
openDebugRun,
|
||||
focusedGrid,
|
||||
selectedComponent,
|
||||
history
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
selectedComponent
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
const loading = {
|
||||
publish: false,
|
||||
@@ -219,18 +221,23 @@
|
||||
return
|
||||
}
|
||||
loading.save = true
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path,
|
||||
requestBody: {
|
||||
value: $app!,
|
||||
summary: $summary,
|
||||
policy
|
||||
}
|
||||
})
|
||||
loading.save = false
|
||||
sendUserToast('App saved')
|
||||
try {
|
||||
await computeTriggerables()
|
||||
await AppService.updateApp({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path,
|
||||
requestBody: {
|
||||
value: $app!,
|
||||
summary: $summary,
|
||||
policy
|
||||
}
|
||||
})
|
||||
sendUserToast('App saved')
|
||||
loading.save = false
|
||||
} catch (e) {
|
||||
loading.save = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
let selectedJobId: string | undefined = undefined
|
||||
@@ -241,7 +248,10 @@
|
||||
$: selectedJobId && testJobLoader?.watchJob(selectedJobId)
|
||||
$: hasErrors = Object.keys($errorByComponent).length > 0
|
||||
|
||||
let lock = false
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
if (lock) return
|
||||
lock = true
|
||||
switch (event.key) {
|
||||
case 'Z':
|
||||
if (event.ctrlKey) {
|
||||
@@ -252,6 +262,7 @@
|
||||
case 'z':
|
||||
if (event.ctrlKey) {
|
||||
$app = undo(history, $app)
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
@@ -280,6 +291,7 @@
|
||||
// break
|
||||
// }
|
||||
}
|
||||
lock = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -470,8 +482,8 @@
|
||||
<input type="text" placeholder="App summary" class="text-sm w-full" bind:value={$summary} />
|
||||
</div>
|
||||
<UndoRedo
|
||||
undoProps={{ disabled: $history.index === 0 }}
|
||||
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
|
||||
undoProps={{ disabled: $history?.index === 0 }}
|
||||
redoProps={{ disabled: $history && $history?.index === $history.history.length - 1 }}
|
||||
on:undo={() => {
|
||||
$app = undo(history, $app)
|
||||
}}
|
||||
@@ -485,12 +497,12 @@
|
||||
<ToggleButton position="left" value="dnd" size="xs">
|
||||
<div class="inline-flex gap-1 items-center">
|
||||
<Pencil size={14} />
|
||||
<span class="hidden md:inline">Editor</span>
|
||||
<span class="hidden lg:inline">Editor</span>
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value="preview" size="xs">
|
||||
<div class="inline-flex gap-1 items-center">
|
||||
<Eye size={14} /> <span class="hidden md:inline">Preview</span>
|
||||
<Eye size={14} /> <span class="hidden lg:inline">Preview</span>
|
||||
</div>
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
@@ -507,42 +519,46 @@
|
||||
</div>
|
||||
|
||||
<div class="hidden lg:block">
|
||||
<ToggleButtonGroup bind:selected={$app.fullscreen}>
|
||||
<ToggleButton position="left" value={false} size="xs">
|
||||
<div class="flex gap-1 justify-start items-center">
|
||||
<AlignHorizontalSpaceAround size={14} />
|
||||
<Tooltip light class="mb-0.5">
|
||||
The max width is 1168px and the content stay centered instead of taking the full page
|
||||
width
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value={true} size="xs">
|
||||
<Expand size={14} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
{#if $app}
|
||||
<ToggleButtonGroup bind:selected={$app.fullscreen}>
|
||||
<ToggleButton position="left" value={false} size="xs">
|
||||
<div class="flex gap-1 justify-start items-center">
|
||||
<AlignHorizontalSpaceAround size={14} />
|
||||
<Tooltip light class="mb-0.5">
|
||||
The max width is 1168px and the content stay centered instead of taking the full
|
||||
page width
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ToggleButton>
|
||||
<ToggleButton position="right" value={true} size="xs">
|
||||
<Expand size={14} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $focusedGrid !== undefined}
|
||||
<Badge color="indigo">
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
|
||||
<button
|
||||
on:click={() => {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</Badge>
|
||||
<div class="hidden lg:block">
|
||||
<Badge color="indigo">
|
||||
<div class="flex flex-row gap-2 justify-center items-center">
|
||||
<div>{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}</div>
|
||||
<button
|
||||
on:click={() => {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
btnClasses="!text-gray-700 !bg-transparent hover:!bg-gray-400/20 !p-[6px] hidden lg:block"
|
||||
btnClasses="hidden lg:block"
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'JSON',
|
||||
@@ -567,9 +583,7 @@
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<MoreVertical size={20} />
|
||||
</Dropdown>
|
||||
/>
|
||||
<span class="hidden md:inline">
|
||||
<Button on:click={() => (inputsDrawerOpen = true)} color="light" size="xs" variant="border">
|
||||
<span class="flex gap-2">
|
||||
@@ -586,7 +600,7 @@
|
||||
variant="border"
|
||||
startIcon={{ icon: faBug }}
|
||||
>
|
||||
<span class="hidden md:inline">Debug Runs</span>
|
||||
<span class="hidden xl:inline">Debug Runs</span>
|
||||
</Button>
|
||||
</span>
|
||||
<AppExportButton bind:this={appExport} />
|
||||
@@ -597,7 +611,7 @@
|
||||
variant="border"
|
||||
startIcon={{ icon: faExternalLink }}
|
||||
>
|
||||
<span class="hidden md:inline">Publish</span>
|
||||
<span class="hidden xl:inline">Publish</span>
|
||||
</Button>
|
||||
{#if appPath == ''}
|
||||
<Button
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import AppComponentInput from './AppComponentInput.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
|
||||
const { app } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let resourceOnly: boolean = true
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type {
|
||||
App,
|
||||
AppEditorContext,
|
||||
AppViewerContext,
|
||||
ConnectingInput,
|
||||
EditorBreakpoint,
|
||||
EditorMode
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
setContext<AppViewerContext>('AppViewerContext', {
|
||||
worldStore,
|
||||
staticOutputs,
|
||||
app: appStore,
|
||||
@@ -61,9 +62,13 @@
|
||||
openDebugRun: writable(undefined),
|
||||
focusedGrid: writable(undefined),
|
||||
stateId: writable(0),
|
||||
parentWidth: writable(0),
|
||||
history: writable<any>(undefined),
|
||||
componentControl: writable({})
|
||||
parentWidth: writable(0)
|
||||
})
|
||||
|
||||
setContext<AppEditorContext>('AppEditorContext', {
|
||||
history: undefined,
|
||||
componentControl: writable({}),
|
||||
pickVariableCallback: writable(undefined)
|
||||
})
|
||||
|
||||
let mounted = false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { Anchor, Bug, Expand, Move } from 'lucide-svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const { errorByComponent, openDebugRun } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { errorByComponent, openDebugRun } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$: error = Object.values($errorByComponent).find((e) => e.componentId === component.id)
|
||||
|
||||
@@ -41,13 +41,14 @@
|
||||
|
||||
{#if pointerdown || selected || hover}
|
||||
<button
|
||||
title="Position locking"
|
||||
title="Lock Position"
|
||||
class={classNames(
|
||||
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[2.5rem] z-50 cursor-pointer',
|
||||
' hover:bg-gray-300',
|
||||
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
|
||||
)}
|
||||
on:click={() => dispatch('lock')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if locked}
|
||||
<Anchor aria-label="Unlock position" size={14} class="text-orange-500" />
|
||||
@@ -56,15 +57,16 @@
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
title="Position locking"
|
||||
title="Expand"
|
||||
class={classNames(
|
||||
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[0.5rem] z-50 cursor-pointer',
|
||||
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[4.5rem] z-50 cursor-pointer',
|
||||
' hover:bg-gray-300',
|
||||
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
|
||||
)}
|
||||
on:click={() => dispatch('expand')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<Expand aria-label="Lock position" size={14} />
|
||||
<Expand aria-label="Expand position" size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -73,7 +75,7 @@
|
||||
title="Move"
|
||||
on:mousedown|stopPropagation|capture
|
||||
class={classNames(
|
||||
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[4.5rem] z-50 cursor-move',
|
||||
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[0.5rem] z-50 cursor-move',
|
||||
'bg-gray-200/80'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getContext, afterUpdate } from 'svelte'
|
||||
import type { App, AppEditorContext, GridItem } from '../types'
|
||||
import Grid from '@windmill-labs/svelte-grid'
|
||||
import type { App, AppEditorContext, AppViewerContext, GridItem } from '../types'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -13,6 +12,7 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { push } from '$lib/history'
|
||||
import { expandGriditem, findGridItem } from './appUtils'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
|
||||
export let policy: Policy
|
||||
|
||||
@@ -26,9 +26,10 @@
|
||||
summary,
|
||||
focusedGrid,
|
||||
parentWidth,
|
||||
history,
|
||||
breakpoint
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
} = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
// The drag is disabled when the user is connecting an input
|
||||
// or when the user is previewing the app
|
||||
@@ -107,13 +108,17 @@
|
||||
let pointerdown = false
|
||||
let lastapp: App | undefined = undefined
|
||||
const onpointerdown = () => {
|
||||
lastapp = JSON.parse(JSON.stringify($app))
|
||||
pointerdown = true
|
||||
if (history) {
|
||||
lastapp = JSON.parse(JSON.stringify($app))
|
||||
pointerdown = true
|
||||
}
|
||||
}
|
||||
const onpointerup = () => {
|
||||
pointerdown = false
|
||||
if (!deepEqual(lastapp, $app)) {
|
||||
push(history, lastapp, false, true)
|
||||
if (history) {
|
||||
pointerdown = false
|
||||
if (!deepEqual(lastapp, $app)) {
|
||||
push(history, lastapp, false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,11 +215,13 @@
|
||||
if (gridItem) {
|
||||
toggleFixed(gridItem)
|
||||
}
|
||||
$app = $app
|
||||
}}
|
||||
on:expand={() => {
|
||||
push(history, $app)
|
||||
$selectedComponent = gridComponent.data.id
|
||||
expandGriditem($app.grid, gridComponent, $breakpoint)
|
||||
$app = { ...$app }
|
||||
$app = $app
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, GridItem } from '../types'
|
||||
import type { AppViewerContext, GridItem } from '../types'
|
||||
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
|
||||
|
||||
const { selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
export let gridItems: GridItem[]
|
||||
export let parent: string | undefined
|
||||
</script>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { ChevronDown, RefreshCw } from 'lucide-svelte'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import Button from '../../common/button/Button.svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import type { AppViewerContext } from '../types'
|
||||
|
||||
const { runnableComponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
|
||||
let loading: boolean = false
|
||||
let timeout: NodeJS.Timer | undefined = undefined
|
||||
let interval: number | undefined = undefined
|
||||
@@ -14,14 +14,14 @@
|
||||
$: componentNumber = Object.keys($runnableComponents).length
|
||||
|
||||
function onClick(stopAfterClear = true) {
|
||||
if(timeout) {
|
||||
if (timeout) {
|
||||
clearInterval(timeout)
|
||||
timeout = undefined
|
||||
shouldRefresh = false
|
||||
if(stopAfterClear) return;
|
||||
if (stopAfterClear) return
|
||||
}
|
||||
refresh()
|
||||
if(interval) {
|
||||
if (interval) {
|
||||
shouldRefresh = true
|
||||
timeout = setInterval(refresh, interval)
|
||||
}
|
||||
@@ -44,12 +44,12 @@
|
||||
}
|
||||
|
||||
function visChange() {
|
||||
if(document.visibilityState === 'hidden') {
|
||||
if(timeout) {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
if (timeout) {
|
||||
clearInterval(timeout)
|
||||
timeout = undefined
|
||||
}
|
||||
} else if(shouldRefresh) {
|
||||
} else if (shouldRefresh) {
|
||||
timeout = setInterval(refresh, interval)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@
|
||||
document.addEventListener('visibilitychange', visChange)
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', visChange)
|
||||
if(timeout) clearInterval(timeout)
|
||||
if (timeout) clearInterval(timeout)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -66,17 +66,19 @@
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
on:click={() => onClick()}
|
||||
color="{timeout ? 'blue' : 'light'}"
|
||||
variant="{timeout ? 'contained' : 'border'}"
|
||||
color={timeout ? 'blue' : 'light'}
|
||||
variant={timeout ? 'contained' : 'border'}
|
||||
size="xs"
|
||||
btnClasses="!rounded-r-none {timeout ? '!border !border-blue-500' : ''}"
|
||||
title="Refresh {componentNumber} component{componentNumber > 1 ? 's' : ''} {interval ? `every ${interval / 1000} seconds` : 'once'}"
|
||||
title="Refresh {componentNumber} component{componentNumber > 1 ? 's' : ''} {interval
|
||||
? `every ${interval / 1000} seconds`
|
||||
: 'once'}"
|
||||
>
|
||||
<RefreshCw class={loading ? 'animate-spin' : ''} size={16} />
|
||||
</Button>
|
||||
<Dropdown
|
||||
btnClasses="!rounded-l-none !border-l-0 min-w-[4rem] !px-2"
|
||||
color="{timeout ? 'blue' : 'light'}"
|
||||
color={timeout ? 'blue' : 'light'}
|
||||
variant="border"
|
||||
dropdownItems={[
|
||||
{
|
||||
@@ -86,7 +88,7 @@
|
||||
...[1, 2, 3, 4, 5, 6].map((i) => ({
|
||||
displayName: `Every ${i * 5} seconds`,
|
||||
action: () => setInter(i * 5000)
|
||||
})),
|
||||
}))
|
||||
]}
|
||||
>
|
||||
<span class="grow text center">{interval ? `${interval / 1000}s` : 'once'}</span>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext } from '../types'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import GridPanel from './GridPanel.svelte'
|
||||
import PanelSection from './settingsPanel/common/PanelSection.svelte'
|
||||
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
|
||||
|
||||
const { selectedComponent, app, stateId } = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { selectedComponent, app, stateId } = getContext<AppViewerContext>('AppViewerContext')
|
||||
</script>
|
||||
|
||||
{#if $app.grid}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Grid from '@windmill-labs/svelte-grid'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
|
||||
import type { AppEditorContext, GridItem } from '../types'
|
||||
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
|
||||
import Component from './component/Component.svelte'
|
||||
import { expandGriditem, findGridItem } from './appUtils'
|
||||
import { push } from '$lib/history'
|
||||
import Grid from '../svelte-grid/Grid.svelte'
|
||||
|
||||
export let containerHeight: number
|
||||
let classes = ''
|
||||
@@ -22,16 +22,10 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const {
|
||||
app,
|
||||
connectingInput,
|
||||
selectedComponent,
|
||||
focusedGrid,
|
||||
mode,
|
||||
parentWidth,
|
||||
breakpoint,
|
||||
history
|
||||
} = getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, connectingInput, selectedComponent, focusedGrid, mode, parentWidth, breakpoint } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
$: highlight = id === $focusedGrid?.parentComponentId && shouldHighlight
|
||||
|
||||
@@ -69,6 +63,7 @@
|
||||
if (fComponent) {
|
||||
fComponent = toggleFixed(fComponent)
|
||||
}
|
||||
$app = $app
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
@@ -142,11 +137,11 @@
|
||||
if (!parentGridItem) {
|
||||
return
|
||||
}
|
||||
|
||||
$selectedComponent = gridComponent.data.id
|
||||
push(history, $app)
|
||||
|
||||
expandGriditem(subGrid, gridComponent, $breakpoint, parentGridItem)
|
||||
$app = { ...$app }
|
||||
$app = $app
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { getNextId } from '$lib/components/flows/flowStateUtils'
|
||||
import type { App, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
|
||||
import type { App, ConnectingInput, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
|
||||
import { getRecommendedDimensionsByComponent, type AppComponent } from './component'
|
||||
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
|
||||
import { gridColumns } from '../gridUtils'
|
||||
import { allItems } from '../utils'
|
||||
import type { Output, World } from '../rx'
|
||||
import gridHelp from '../svelte-grid/utils/helper'
|
||||
|
||||
function findGridItemById(
|
||||
root: GridItem[],
|
||||
@@ -18,7 +19,7 @@ function findGridItemById(
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function findGridItemParentId(app: App, id: string): string | undefined {
|
||||
export function findGridItemParentGrid(app: App, id: string): string | undefined {
|
||||
const gridItem = app.grid.find((x) => x.id === id)
|
||||
if (gridItem) {
|
||||
return undefined
|
||||
@@ -27,12 +28,23 @@ export function findGridItemParentId(app: App, id: string): string | undefined {
|
||||
const subGrid = app.subgrids[key]
|
||||
const gridItem = subGrid.find((x) => x.id === id)
|
||||
if (gridItem) {
|
||||
return key.split('-')[0]
|
||||
return key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isIdInsideGriditem(app: App, gridItem: GridItem, id: string | undefined): boolean {
|
||||
const path: string[] = []
|
||||
let currentId = id
|
||||
while (currentId) {
|
||||
path.push(currentId)
|
||||
currentId = findGridItemParentGrid(app, currentId)?.split('-')[0]
|
||||
}
|
||||
|
||||
return path.includes(gridItem.id)
|
||||
}
|
||||
|
||||
export function findGridItem(app: App, id: string): GridItem | undefined {
|
||||
return findGridItemById(app.grid, app.subgrids, id)
|
||||
}
|
||||
@@ -46,10 +58,6 @@ export function getNextGridItemId(app: App): string {
|
||||
}
|
||||
|
||||
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
|
||||
const appComponent = data
|
||||
|
||||
appComponent.id = id
|
||||
|
||||
const newComponent = {
|
||||
resizable: true,
|
||||
draggable: true,
|
||||
@@ -57,7 +65,8 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
|
||||
y: 0
|
||||
}
|
||||
|
||||
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
|
||||
let newData: AppComponent = JSON.parse(JSON.stringify(data))
|
||||
newData.id = id
|
||||
|
||||
const newItem: GridItem = {
|
||||
data: newData,
|
||||
@@ -65,7 +74,7 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
|
||||
}
|
||||
|
||||
gridColumns.forEach((column) => {
|
||||
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
|
||||
const rec = getRecommendedDimensionsByComponent(newData.type, column)
|
||||
|
||||
newItem[column] = {
|
||||
...newComponent,
|
||||
@@ -84,6 +93,16 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
|
||||
return newItem
|
||||
}
|
||||
|
||||
export function getGridItems(app: App, focusedGrid: FocusedGrid | undefined): GridItem[] {
|
||||
if (!focusedGrid) {
|
||||
return app.grid
|
||||
} else {
|
||||
const { parentComponentId, subGridIndex } = focusedGrid
|
||||
const key = `${parentComponentId}-${subGridIndex ?? 0}`
|
||||
return app?.subgrids?.[key] ?? []
|
||||
}
|
||||
}
|
||||
|
||||
export function insertNewGridItem(
|
||||
app: App,
|
||||
data: AppComponent,
|
||||
@@ -96,18 +115,6 @@ export function insertNewGridItem(
|
||||
app.subgrids = {}
|
||||
}
|
||||
|
||||
if (!focusedGrid) {
|
||||
const newItem = createNewGridItem(app.grid, id, data)
|
||||
app.grid.push(newItem)
|
||||
} else {
|
||||
const { parentComponentId, subGridIndex } = focusedGrid
|
||||
|
||||
const key = `${parentComponentId}-${subGridIndex ?? 0}`
|
||||
|
||||
const subGrid = app.subgrids[key] ?? []
|
||||
subGrid.push(createNewGridItem(subGrid, id, data))
|
||||
app.subgrids[key] = subGrid
|
||||
}
|
||||
// We only want to set subgrids when we are not moving
|
||||
if (!keepId) {
|
||||
for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) {
|
||||
@@ -115,6 +122,17 @@ export function insertNewGridItem(
|
||||
}
|
||||
}
|
||||
|
||||
const key = focusedGrid
|
||||
? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}`
|
||||
: undefined
|
||||
let grid = focusedGrid ? app.subgrids[key!] : app.grid
|
||||
|
||||
const newItem = createNewGridItem(grid, id, data)
|
||||
grid.push(newItem)
|
||||
|
||||
if (focusedGrid) {
|
||||
app.subgrids[key!] = grid
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -169,28 +187,6 @@ export function deleteGridItem(
|
||||
return components
|
||||
}
|
||||
|
||||
export function duplicateGridItem(
|
||||
app: App,
|
||||
parent: string | undefined,
|
||||
id: string
|
||||
): string | undefined {
|
||||
const gridItem = findGridItem(app, id)
|
||||
|
||||
if (gridItem) {
|
||||
const newId = getNextGridItemId(app)
|
||||
const newItem = JSON.parse(JSON.stringify(gridItem))
|
||||
newItem.id = newId
|
||||
newItem.data.id = newId
|
||||
|
||||
let focusedGrid = parent
|
||||
? { parentComponentId: parent.split('-')[0], subGridIndex: Number(parent.split('-')[1]) }
|
||||
: undefined
|
||||
|
||||
return insertNewGridItem(app, newItem.data, focusedGrid)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
type AvailableSpace = {
|
||||
left: number
|
||||
right: number
|
||||
@@ -206,7 +202,7 @@ export function findAvailableSpace(
|
||||
): AvailableSpace | undefined {
|
||||
if (gridItem) {
|
||||
const breakpoint = editorBreakpoint === 'sm' ? 3 : 12
|
||||
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 12
|
||||
const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 16
|
||||
const maxWidth = 12
|
||||
|
||||
const availableSpace = {
|
||||
@@ -297,6 +293,25 @@ function isOverlapping(item1: any, item2: any) {
|
||||
)
|
||||
}
|
||||
|
||||
type Outputtable<Type> = {
|
||||
-readonly [Property in keyof Type]: Output<Type[Property]>
|
||||
}
|
||||
|
||||
export function initOutput<I extends Record<string, any>>(
|
||||
world: World,
|
||||
id: string,
|
||||
init: I
|
||||
): Outputtable<I> {
|
||||
const output = world.outputsById[id] as Outputtable<I>
|
||||
if (init) {
|
||||
for (const key in init) {
|
||||
if (output && output[key] && output[key].peak() == undefined) {
|
||||
output[key].set(init[key] as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
export function expandGriditem(
|
||||
grid: GridItem[],
|
||||
gridComponent: GridItem,
|
||||
@@ -311,10 +326,60 @@ export function expandGriditem(
|
||||
|
||||
const { left, right, top, bottom } = availableSpace
|
||||
const width = $breakpoint === 'sm' ? 3 : 12
|
||||
const previousGridItem = JSON.parse(JSON.stringify(gridComponent[width]))
|
||||
const item = gridComponent[width]
|
||||
|
||||
gridComponent[width].x = previousGridItem.x - left
|
||||
gridComponent[width].y = previousGridItem.y - top
|
||||
gridComponent[width].w = previousGridItem.w + left + right
|
||||
gridComponent[width].h = previousGridItem.h + top + bottom
|
||||
item.x = item.x - left
|
||||
item.y = item.y - top
|
||||
item.w = item.w + left + right
|
||||
item.h = item.h + top + bottom
|
||||
}
|
||||
|
||||
export function sortGridItemsPosition(
|
||||
gridItems: GridItem[],
|
||||
breakpoint: EditorBreakpoint
|
||||
): GridItem[] {
|
||||
return gridItems.sort((a: GridItem, b: GridItem) => {
|
||||
const width = breakpoint === 'lg' ? 12 : 3
|
||||
|
||||
const aX = a[width].x
|
||||
const aY = a[width].y
|
||||
const bX = b[width].x
|
||||
const bY = b[width].y
|
||||
|
||||
if (aY < bY) {
|
||||
return -1
|
||||
} else if (aY > bY) {
|
||||
return 1
|
||||
} else {
|
||||
if (aX < bX) {
|
||||
return -1
|
||||
} else if (aX > bX) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function connectInput(
|
||||
connectingInput: ConnectingInput,
|
||||
componentId: string,
|
||||
path: string
|
||||
): ConnectingInput {
|
||||
if (connectingInput) {
|
||||
connectingInput = {
|
||||
opened: false,
|
||||
input: {
|
||||
connection: {
|
||||
componentId,
|
||||
path
|
||||
},
|
||||
type: 'connected'
|
||||
},
|
||||
hoveredComponent: undefined
|
||||
}
|
||||
}
|
||||
|
||||
return connectingInput
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import ComponentHeader from '../ComponentHeader.svelte'
|
||||
import type { AppComponent } from './components'
|
||||
import {
|
||||
@@ -49,7 +49,7 @@
|
||||
export let render: boolean
|
||||
|
||||
const { staticOutputs, mode, connectingInput, app, errorByComponent } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let hover = false
|
||||
let initializing: boolean | undefined = undefined
|
||||
let componentContainerHeight: number = 0
|
||||
@@ -222,7 +222,7 @@
|
||||
recomputeIds={component.recomputeIds}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'selectcomponent'}
|
||||
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
|
||||
<AppSelect
|
||||
id={component.id}
|
||||
verticalAlignment={component.verticalAlignment}
|
||||
@@ -397,7 +397,6 @@
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
panes={component.panes}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
/>
|
||||
@@ -407,7 +406,6 @@
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
panes={component.panes}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
{componentContainerHeight}
|
||||
horizontal={true}
|
||||
{render}
|
||||
|
||||
@@ -1,21 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, EditorBreakpoint, GridItem } from '../../types'
|
||||
import { findGridItemParentId } from '../appUtils'
|
||||
import {
|
||||
findGridItem,
|
||||
findGridItemParentGrid,
|
||||
getAllSubgridsAndComponentIds,
|
||||
insertNewGridItem,
|
||||
sortGridItemsPosition
|
||||
} from '../appUtils'
|
||||
import type { AppEditorContext, AppViewerContext, EditorBreakpoint, GridItem } from '../../types'
|
||||
import { push } from '$lib/history'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
|
||||
const { app, selectedComponent, breakpoint, componentControl, focusedGrid } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, selectedComponent, breakpoint, focusedGrid } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { componentControl, history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let tempGridItem: GridItem | undefined = undefined
|
||||
let copiedGridItem: GridItem | undefined = undefined
|
||||
|
||||
function getSortedGridItemsOfChildren(): GridItem[] {
|
||||
if (!$focusedGrid) {
|
||||
return sortGridItems($app.grid, $breakpoint)
|
||||
return sortGridItemsPosition($app.grid, $breakpoint)
|
||||
}
|
||||
|
||||
if (!$app.subgrids) {
|
||||
return []
|
||||
}
|
||||
|
||||
return sortGridItems(
|
||||
return sortGridItemsPosition(
|
||||
$app.subgrids[`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`],
|
||||
$breakpoint
|
||||
)
|
||||
@@ -23,14 +36,14 @@
|
||||
|
||||
function getSortedGridItemsOfGrid(): GridItem[] {
|
||||
if ($app.grid.find((item) => item.id === $selectedComponent)) {
|
||||
return sortGridItems($app.grid, $breakpoint)
|
||||
return sortGridItemsPosition($app.grid, $breakpoint)
|
||||
}
|
||||
|
||||
if (!$app.subgrids) {
|
||||
return []
|
||||
}
|
||||
|
||||
return sortGridItems(
|
||||
return sortGridItemsPosition(
|
||||
Object.values($app.subgrids ?? {}).find((grid) =>
|
||||
grid.find((item) => item.id === $selectedComponent)
|
||||
) ?? [],
|
||||
@@ -38,7 +51,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
function left(event) {
|
||||
function left(event: KeyboardEvent) {
|
||||
if (!$componentControl[$selectedComponent!]?.left?.()) {
|
||||
const sortedGridItems = getSortedGridItemsOfGrid()
|
||||
const currentIndex = sortedGridItems.findIndex((item) => item.id === $selectedComponent)
|
||||
@@ -46,13 +59,12 @@
|
||||
if (currentIndex !== -1 && currentIndex > 0) {
|
||||
$selectedComponent = sortedGridItems[currentIndex - 1].id
|
||||
}
|
||||
event.preventDefault()
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function right(event) {
|
||||
function right(event: KeyboardEvent) {
|
||||
let r = $componentControl[$selectedComponent!]?.right?.()
|
||||
if (!r) {
|
||||
const sortedGridItems = getSortedGridItemsOfGrid()
|
||||
@@ -61,15 +73,15 @@
|
||||
if (currentIndex !== -1 && currentIndex < sortedGridItems.length - 1) {
|
||||
$selectedComponent = sortedGridItems[currentIndex + 1].id
|
||||
}
|
||||
event.preventDefault()
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function down(event) {
|
||||
function down(event: KeyboardEvent) {
|
||||
if (!$focusedGrid) {
|
||||
$selectedComponent = getSortedGridItemsOfChildren()[0]?.id
|
||||
event.preventDefault()
|
||||
} else if ($app.subgrids) {
|
||||
const index = $focusedGrid?.subGridIndex ?? 0
|
||||
const subgrid = $app.subgrids[`${$selectedComponent}-${index}`]
|
||||
@@ -78,7 +90,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const sortedGridItems = sortGridItems(subgrid, $breakpoint)
|
||||
const sortedGridItems = sortGridItemsPosition(subgrid, $breakpoint)
|
||||
|
||||
if (sortedGridItems) {
|
||||
$selectedComponent = sortedGridItems[0].id
|
||||
@@ -87,22 +99,89 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleArrowUp(event: KeyboardEvent) {
|
||||
if (!$selectedComponent) return
|
||||
let parentId = findGridItemParentGrid($app, $selectedComponent)?.split('-')[0]
|
||||
|
||||
if (parentId) {
|
||||
$selectedComponent = parentId
|
||||
} else {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopy(event: KeyboardEvent) {
|
||||
if (!$selectedComponent) {
|
||||
return
|
||||
}
|
||||
tempGridItem = undefined
|
||||
copiedGridItem = findGridItem($app, $selectedComponent)
|
||||
}
|
||||
|
||||
function handleCut(event: KeyboardEvent) {
|
||||
if (!$selectedComponent) {
|
||||
return
|
||||
}
|
||||
push(history, $app)
|
||||
|
||||
const gridItem = findGridItem($app, $selectedComponent)
|
||||
copiedGridItem = gridItem
|
||||
|
||||
if (!gridItem) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the grid item in a temp variable so we can paste it later
|
||||
tempGridItem = gridItem
|
||||
}
|
||||
|
||||
function handlePaste(event: KeyboardEvent) {
|
||||
push(history, $app)
|
||||
|
||||
if (tempGridItem) {
|
||||
if (
|
||||
$focusedGrid &&
|
||||
getAllSubgridsAndComponentIds($app, tempGridItem.data)[0].includes(
|
||||
`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`
|
||||
)
|
||||
) {
|
||||
sendUserToast('Cannot paste a component into itself', true)
|
||||
return
|
||||
}
|
||||
let parentGrid = findGridItemParentGrid($app, tempGridItem.data.id)
|
||||
const grid = parentGrid ? $app.subgrids![parentGrid] : $app.grid
|
||||
let idx = grid.findIndex((item) => {
|
||||
return item.id == tempGridItem!.data.id
|
||||
})
|
||||
if (idx > -1) {
|
||||
grid.splice(idx, 1)
|
||||
}
|
||||
|
||||
insertNewGridItem($app, tempGridItem.data, $focusedGrid, true)
|
||||
copiedGridItem = tempGridItem
|
||||
tempGridItem = undefined
|
||||
} else if (copiedGridItem) {
|
||||
insertNewGridItem($app, copiedGridItem.data, $focusedGrid, false)
|
||||
}
|
||||
|
||||
$app = $app
|
||||
}
|
||||
|
||||
function keydown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
$selectedComponent = undefined
|
||||
event.preventDefault()
|
||||
handleEscape(event)
|
||||
break
|
||||
|
||||
case 'ArrowUp': {
|
||||
if (!$selectedComponent) return
|
||||
let parentId = findGridItemParentId($app, $selectedComponent)
|
||||
if (parentId) {
|
||||
$selectedComponent = parentId
|
||||
} else {
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
handleArrowUp(event)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -121,35 +200,28 @@
|
||||
break
|
||||
}
|
||||
|
||||
case 'c':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
handleCopy(event)
|
||||
}
|
||||
break
|
||||
|
||||
case 'v':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
handlePaste(event)
|
||||
}
|
||||
break
|
||||
|
||||
case 'x':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
handleCut(event)
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function sortGridItems(gridItems: GridItem[], breakpoint: EditorBreakpoint): GridItem[] {
|
||||
return gridItems.sort((a: GridItem, b: GridItem) => {
|
||||
const width = breakpoint === 'lg' ? 12 : 3
|
||||
|
||||
const aX = a[width].x
|
||||
const aY = a[width].y
|
||||
const bX = b[width].x
|
||||
const bY = b[width].y
|
||||
|
||||
if (aY < bY) {
|
||||
return -1
|
||||
} else if (aY > bY) {
|
||||
return 1
|
||||
} else {
|
||||
if (aX < bX) {
|
||||
return -1
|
||||
} else if (aX > bX) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={keydown} />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Size } from '@windmill-labs/svelte-grid'
|
||||
import type { IntRange } from '../../../../common'
|
||||
import type { NARROW_GRID_COLUMNS, WIDE_GRID_COLUMNS } from '../../gridUtils'
|
||||
import { defaultAlignement } from '../componentsPanel/componentDefaultProps'
|
||||
@@ -36,6 +35,7 @@ import {
|
||||
AtSignIcon
|
||||
} from 'lucide-svelte'
|
||||
import type { BaseAppComponent } from '../../types'
|
||||
import type { Size } from '../../svelte-grid/types'
|
||||
|
||||
type BaseComponent<T extends string> = {
|
||||
type: T
|
||||
@@ -74,7 +74,8 @@ export type AggridComponent = BaseComponent<'aggridcomponent'>
|
||||
export type DisplayComponent = BaseComponent<'displaycomponent'>
|
||||
export type ImageComponent = BaseComponent<'imagecomponent'>
|
||||
export type InputComponent = BaseComponent<'inputcomponent'>
|
||||
export type SelectComponent = BaseComponent<'selectcomponent'>
|
||||
export type SelectComponent = BaseComponent<'resourceselectcomponent'>
|
||||
export type ResourceSelectComponent = BaseComponent<'selectcomponent'>
|
||||
export type MultiSelectComponent = BaseComponent<'multiselectcomponent'>
|
||||
export type CheckboxComponent = BaseComponent<'checkboxcomponent'>
|
||||
export type RadioComponent = BaseComponent<'radiocomponent'>
|
||||
@@ -116,6 +117,7 @@ export type AppComponent = BaseAppComponent &
|
||||
| PieChartComponent
|
||||
| ScatterChartComponent
|
||||
| SelectComponent
|
||||
| ResourceSelectComponent
|
||||
| MultiSelectComponent
|
||||
| CheckboxComponent
|
||||
| FormComponent
|
||||
@@ -911,7 +913,7 @@ Hello \${ctx.username}
|
||||
}
|
||||
},
|
||||
multiselectcomponent: {
|
||||
name: 'MultiSelect',
|
||||
name: 'Multi Select',
|
||||
icon: List,
|
||||
dims: '2:1-3:1',
|
||||
data: {
|
||||
@@ -940,6 +942,36 @@ Hello \${ctx.username}
|
||||
softWrap: true
|
||||
}
|
||||
},
|
||||
resourceselectcomponent: {
|
||||
name: 'Resource Select',
|
||||
icon: List,
|
||||
dims: '2:1-3:1',
|
||||
data: {
|
||||
verticalAlignment: 'center',
|
||||
id: '',
|
||||
type: 'resourceselectcomponent',
|
||||
componentInput: undefined,
|
||||
configuration: {
|
||||
items: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'labeledresource',
|
||||
value: []
|
||||
},
|
||||
placeholder: {
|
||||
type: 'static',
|
||||
fieldType: 'text',
|
||||
value: 'Select an item',
|
||||
onlyStatic: true
|
||||
}
|
||||
},
|
||||
customCss: {
|
||||
input: { style: '' }
|
||||
} as const,
|
||||
card: false,
|
||||
softWrap: true
|
||||
}
|
||||
},
|
||||
numberinputcomponent: {
|
||||
name: 'Number',
|
||||
icon: Binary,
|
||||
|
||||
@@ -72,14 +72,14 @@ return {
|
||||
},
|
||||
displaycomponent: {
|
||||
deno: `export async function main() {
|
||||
return {
|
||||
"foo": 42
|
||||
}
|
||||
return {
|
||||
"foo": 42
|
||||
}
|
||||
}`,
|
||||
python3: `def main():
|
||||
return {
|
||||
"foo": 42
|
||||
}`
|
||||
return {
|
||||
"foo": 42
|
||||
}`
|
||||
},
|
||||
htmlcomponent: {
|
||||
deno: `export async function main() {
|
||||
|
||||
@@ -32,6 +32,7 @@ const inputs: ComponentSet = {
|
||||
'fileinputcomponent',
|
||||
'checkboxcomponent',
|
||||
'selectcomponent',
|
||||
'resourceselectcomponent',
|
||||
'multiselectcomponent',
|
||||
]
|
||||
} as const
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { AppEditorContext } from '../../types'
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
import { getContext } from 'svelte'
|
||||
import { fade, slide } from 'svelte/transition'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
@@ -10,8 +10,9 @@
|
||||
import { push } from '$lib/history'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
const { app, selectedComponent, focusedGrid, history } =
|
||||
getContext<AppEditorContext>('AppEditorContext')
|
||||
const { app, selectedComponent, focusedGrid } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
function addComponent(appComponentType: AppComponent['type']): void {
|
||||
push(history, $app)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user