mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-17 08:02:25 +00:00
Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9007072274 | ||
|
|
fba9e7ef03 | ||
|
|
72109b01d7 | ||
|
|
e7428bf84d | ||
|
|
56b417c669 | ||
|
|
8e1e37bf9e | ||
|
|
5e965aec15 | ||
|
|
04ffbf8c26 | ||
|
|
b601883bc0 | ||
|
|
81ed965812 | ||
|
|
e1ff00117c | ||
|
|
55d599d4b6 | ||
|
|
46c784574a | ||
|
|
3063001491 | ||
|
|
d30979d04e | ||
|
|
7bf9e25ede | ||
|
|
f406da9976 | ||
|
|
07f91af535 | ||
|
|
cb559d6083 | ||
|
|
63af3ce0b1 | ||
|
|
3bc891a0dd | ||
|
|
5569e4d495 | ||
|
|
7f290bbf6a | ||
|
|
69fc8a98ae | ||
|
|
eff2a4c496 | ||
|
|
5219062cc1 | ||
|
|
318b654106 | ||
|
|
731299ee14 | ||
|
|
dde3c5bef3 | ||
|
|
3ada264c4a | ||
|
|
e2cd8dbdf0 | ||
|
|
487d84bd7f | ||
|
|
82d37d6ace | ||
|
|
a85ebfbbf4 | ||
|
|
33b0e71523 | ||
|
|
630e54f65c | ||
|
|
b26ebb3a79 | ||
|
|
9b3ec10c12 | ||
|
|
ebe3582ced | ||
|
|
24b6003780 | ||
|
|
478d3fbf4a | ||
|
|
47c8b334ef | ||
|
|
44b26d2cce | ||
|
|
4ab622deaa | ||
|
|
c13fa65354 | ||
|
|
56d1da78fd | ||
|
|
d0ee16b1b9 | ||
|
|
b175730324 | ||
|
|
874dbd9dfa | ||
|
|
25f9369d5e | ||
|
|
64d0c32443 | ||
|
|
50607c7625 | ||
|
|
c8a959691c | ||
|
|
edcf2d43a2 | ||
|
|
3bf0bc64af | ||
|
|
d48b2dd886 | ||
|
|
03f6db0cb8 | ||
|
|
4ee4ff78d3 | ||
|
|
d45c1f69d4 | ||
|
|
96597d3d6b | ||
|
|
9a236ef3eb | ||
|
|
8806870b1b | ||
|
|
11881b48cc | ||
|
|
61983a5bbf | ||
|
|
0456272e3f | ||
|
|
1d03a95bef | ||
|
|
3028325615 | ||
|
|
b3e00b7fdc | ||
|
|
653241c43e | ||
|
|
7c842c88bf | ||
|
|
d7ef7fe360 | ||
|
|
a87b4a52ed | ||
|
|
9736355d5f | ||
|
|
0329d91ee9 | ||
|
|
7ef8100250 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.83.0
|
||||
toolchain: 1.85.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
+307
-12
@@ -8,22 +8,317 @@ on:
|
||||
jobs:
|
||||
benchmark_single:
|
||||
runs-on: ubicloud-standard-8
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_INITDB_ARGS: "-c shared_buffers=2GB -c work_mem=32MB -c effective_cache_size=4GB"
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
--shm-size=2g
|
||||
|
||||
|
||||
windmill:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5 --health-cmd "curl
|
||||
http://localhost:8000/api/version"
|
||||
ports:
|
||||
- 8000:8000
|
||||
steps:
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: benchmark
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
docker run -d --network=host -e POSTGRES_PASSWORD=changeme -e POSTGRES_USER=postgres -e POSTGRES_DB=windmill -e POSTGRES_INITDB_ARGS="-c log_duration=on -c log_statement=all -c log_min_duration_statement=0 -c shared_buffers=2GB -c work_mem=32MB -c effective_cache_size=4GB -c shared_preload_libraries=auto_explain -c auto_explain.log_min_duration=5 -c auto_explain.log_analyze=on -c auto_explain.log_timing=on -c auto_explain.log_buffers=on -c auto_explain.log_verbose=on \
|
||||
-c log_statement=all \
|
||||
-c log_min_duration_statement=0 \
|
||||
-c shared_buffers=2GB \
|
||||
-c work_mem=32MB \
|
||||
-c effective_cache_size=4GB" \
|
||||
postgres
|
||||
sleep 5
|
||||
run: deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
|
||||
-c
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
|
||||
- name: Save benchmark results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_single
|
||||
path: |
|
||||
*.json
|
||||
|
||||
docker run -d -it --network=host -e DATABASE_URL=postgres://postgres:changeme@localhost/windmill ghcr.io/windmill-labs/windmill:main
|
||||
sleep 10
|
||||
deno run --unstable -A -r https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts -c https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
|
||||
benchmark_dedicated:
|
||||
runs-on: ubicloud-standard-8
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_INITDB_ARGS: "-c shared_buffers=2GB -c work_mem=32MB -c effective_cache_size=4GB"
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
windmill:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
WORKER_GROUP: dedicated
|
||||
DEDICATED_WORKER: "admins:f/benchmarks/dedicated"
|
||||
options: >-
|
||||
--pull always --restart unless-stopped --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5 --health-cmd "curl
|
||||
http://localhost:8000/api/version"
|
||||
ports:
|
||||
- 8000:8000
|
||||
steps:
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: benchmark
|
||||
timeout-minutes: 20
|
||||
run: deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
|
||||
--no-warm-up -c
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_dedicated.json
|
||||
- name: Save benchmark results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_dedicated
|
||||
path: |
|
||||
*.json
|
||||
|
||||
benchmark_4workers:
|
||||
runs-on: ubicloud-standard-8
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_INITDB_ARGS: "-c shared_buffers=2GB -c work_mem=32MB -c effective_cache_size=4GB"
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
windmill:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5 --health-cmd "curl
|
||||
http://localhost:8000/api/version"
|
||||
ports:
|
||||
- 8000:8000
|
||||
windmill_1:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_2:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_3:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
steps:
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: benchmark
|
||||
timeout-minutes: 20
|
||||
run: deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
|
||||
-c
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
|
||||
--workers 4
|
||||
--factor 3
|
||||
- name: Save benchmark results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_4workers
|
||||
path: |
|
||||
*.json
|
||||
|
||||
benchmark_8workers:
|
||||
runs-on: ubicloud-standard-8
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_INITDB_ARGS: "-c shared_buffers=2GB -c work_mem=32MB -c effective_cache_size=4GB"
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
windmill:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5 --health-cmd "curl
|
||||
http://localhost:8000/api/version"
|
||||
ports:
|
||||
- 8000:8000
|
||||
windmill_1:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_2:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_3:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_4:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_5:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_6:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
|
||||
windmill_7:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@postgres:5432/windmill
|
||||
LICENSE_KEY: ${{ secrets.WM_LICENSE_KEY_CI }}
|
||||
MODE: worker
|
||||
WORKER_GROUP: main
|
||||
WORKER_TAGS: deno,bun,go,python3,bash,dependency,flow,nativets
|
||||
options: >-
|
||||
--pull always
|
||||
steps:
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: benchmark
|
||||
timeout-minutes: 20
|
||||
run: deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
|
||||
-c
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/suite_config.json
|
||||
--workers 8
|
||||
--factor 3
|
||||
- name: Save benchmark results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: benchmark_8workers
|
||||
path: |
|
||||
*.json
|
||||
|
||||
benchmark_graphs:
|
||||
runs-on: ubicloud
|
||||
needs:
|
||||
- benchmark_single
|
||||
- benchmark_dedicated
|
||||
- benchmark_4workers
|
||||
- benchmark_8workers
|
||||
steps:
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: benchmarks
|
||||
- name: Download benchmark results
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
- name: graphs
|
||||
run: deno run --unstable -A -r
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_graphs.ts
|
||||
-c
|
||||
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/graphs_config.json
|
||||
- name: Push changes
|
||||
run: |
|
||||
ls -la
|
||||
pwd
|
||||
git add .
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "Update benchmarks"
|
||||
git push
|
||||
|
||||
@@ -30,6 +30,12 @@ jobs:
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.85.0
|
||||
override: true
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -32,6 +32,12 @@ jobs:
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.85.0
|
||||
override: true
|
||||
|
||||
- name: Substitute EE code
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
+108
@@ -1,5 +1,113 @@
|
||||
# Changelog
|
||||
|
||||
## [1.473.0](https://github.com/windmill-labs/windmill/compare/v1.472.1...v1.473.0) (2025-03-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* app s3 input anonymous delete ([#5401](https://github.com/windmill-labs/windmill/issues/5401)) ([46c7845](https://github.com/windmill-labs/windmill/commit/46c784574add176cb75d3627c9d3f55b6fb945f8))
|
||||
* track workspace runnables used in flows ([#5369](https://github.com/windmill-labs/windmill/issues/5369)) ([7bf9e25](https://github.com/windmill-labs/windmill/commit/7bf9e25ede82486115eae71865202f85aa931a8d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve db loads by adding index on audit ([e1ff001](https://github.com/windmill-labs/windmill/commit/e1ff00117ca5b66dd0b9365e63b9ff7dc277bc2c))
|
||||
* migrations do not refer to public schema anymore ([#5400](https://github.com/windmill-labs/windmill/issues/5400)) ([3063001](https://github.com/windmill-labs/windmill/commit/3063001491b49a4b6d0cd5985818b32aa4d3f16f))
|
||||
* remove typings_extensions from python sdk ([04ffbf8](https://github.com/windmill-labs/windmill/commit/04ffbf8c266a06c3efcebcbbaee767f0ab0771e2))
|
||||
|
||||
## [1.472.1](https://github.com/windmill-labs/windmill/compare/v1.472.0...v1.472.1) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable bundling using env var ([#5396](https://github.com/windmill-labs/windmill/issues/5396)) ([cb559d6](https://github.com/windmill-labs/windmill/commit/cb559d6083553c400e18e6077002c4891289a8a2))
|
||||
|
||||
## [1.472.0](https://github.com/windmill-labs/windmill/compare/v1.471.1...v1.472.0) (2025-02-26)
|
||||
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* downgrade v8 to fix some rare panics ([5569e4d](https://github.com/windmill-labs/windmill/commit/5569e4d4953a01f2ad03ea8b71e695e833964bea))
|
||||
* **frontend:** markdown shows single backtick in single line code block ([#5391](https://github.com/windmill-labs/windmill/issues/5391)) ([7f290bb](https://github.com/windmill-labs/windmill/commit/7f290bbf6a33e2811dbe2bd8ee905c0fa8e8db3b))
|
||||
* migrate toggle to melt (4/4) ([#5329](https://github.com/windmill-labs/windmill/issues/5329)) ([69fc8a9](https://github.com/windmill-labs/windmill/commit/69fc8a98ae78bc01dc3d97f9732ee28864b323dd))
|
||||
|
||||
## [1.471.1](https://github.com/windmill-labs/windmill/compare/v1.471.0...v1.471.1) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update to rust 1.86.0 ([3ada264](https://github.com/windmill-labs/windmill/commit/3ada264c4ad49f666c3a053eb48c7df294bf085b))
|
||||
|
||||
## [1.471.0](https://github.com/windmill-labs/windmill/compare/v1.470.1...v1.471.0) (2025-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for claude sonnet 3.7 thinking ([#5387](https://github.com/windmill-labs/windmill/issues/5387)) ([487d84b](https://github.com/windmill-labs/windmill/commit/487d84bd7fdc39a2401df4108fca6183189cf38a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** improve pagination handling and filter refreshes ([#5378](https://github.com/windmill-labs/windmill/issues/5378)) ([a85ebfb](https://github.com/windmill-labs/windmill/commit/a85ebfbbf48590812c0931ad93179c322f819849))
|
||||
|
||||
## [1.470.1](https://github.com/windmill-labs/windmill/compare/v1.470.0...v1.470.1) (2025-02-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* multiple app initializations fixes ([630e54f](https://github.com/windmill-labs/windmill/commit/630e54f65c950ec0073b3cdac9974cb666c1ab3f))
|
||||
|
||||
## [1.470.0](https://github.com/windmill-labs/windmill/compare/v1.469.0...v1.470.0) (2025-02-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** set default app refesh interval ([#5380](https://github.com/windmill-labs/windmill/issues/5380)) ([478d3fb](https://github.com/windmill-labs/windmill/commit/478d3fbf4a7e52d19fcb5cf8d601b2eeb3487716))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* multiple app initializations fixes ([24b6003](https://github.com/windmill-labs/windmill/commit/24b600378025632aecb2ca898b63d6032e08eb2e))
|
||||
|
||||
## [1.469.0](https://github.com/windmill-labs/windmill/compare/v1.468.0...v1.469.0) (2025-02-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* limit the number of times a job can be restarted (3) after loss of pings ([c8a9596](https://github.com/windmill-labs/windmill/commit/c8a959691c37350def37fe3eb9f24c6f7789960d))
|
||||
* python-client now support mocked api via `WM_MOCKED_API_FILE` env ([#5372](https://github.com/windmill-labs/windmill/issues/5372)) ([50607c7](https://github.com/windmill-labs/windmill/commit/50607c7625e4a48fb397cff167b41bb6602716c0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve flow editor for vscode extension ([44b26d2](https://github.com/windmill-labs/windmill/commit/44b26d2ccec0c9dd65d1f53b057d031f841d7dba))
|
||||
* improve infinite grid behavior ([56d1da7](https://github.com/windmill-labs/windmill/commit/56d1da78fd3424ae5b4abbb009c7437ea98765ef))
|
||||
|
||||
## [1.468.0](https://github.com/windmill-labs/windmill/compare/v1.467.1...v1.468.0) (2025-02-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add audit logs scope filter in admins workspace ([#5352](https://github.com/windmill-labs/windmill/issues/5352)) ([b3e00b7](https://github.com/windmill-labs/windmill/commit/b3e00b7fdc3ad4c689fc30216accbed05822794c))
|
||||
* add support for | None and Optional in python ([#5361](https://github.com/windmill-labs/windmill/issues/5361)) ([9736355](https://github.com/windmill-labs/windmill/commit/9736355d5f82615100212698c5537997e5a0de39))
|
||||
* make flow lock deployment error visible in UI ([b8e6d0d](https://github.com/windmill-labs/windmill/commit/b8e6d0da79ca57b115e7cb0ccff9f5623b23f3f3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add LOCALAPPDATA env variable to python execution on windows ([8806870](https://github.com/windmill-labs/windmill/commit/8806870b1bf67c2f77beaf04d986cf172c7b4bf4))
|
||||
* fix confirmation modal check on deploy ([3028325](https://github.com/windmill-labs/windmill/commit/3028325615e2f7e5ee3d1b6278580121880db14f))
|
||||
* **frontend:** make html app component content selectable ([#5359](https://github.com/windmill-labs/windmill/issues/5359)) ([f1c5b77](https://github.com/windmill-labs/windmill/commit/f1c5b77d7af8433905937d274b97c2d5cd6c1316))
|
||||
* handle better forced value propagation in apps ([7c842c8](https://github.com/windmill-labs/windmill/commit/7c842c88bf5225b6bc39857109b1b1ba5f99d708))
|
||||
* handle better optional chaining operator ([d45c1f6](https://github.com/windmill-labs/windmill/commit/d45c1f69d48a5ad93f4399ce0150bbff6fd4fa6b))
|
||||
* improve app markdown rendering ([96597d3](https://github.com/windmill-labs/windmill/commit/96597d3d6b3d31298e5582a55e11e1d48edbf175))
|
||||
* improve cancel/back behavior on editors ([0565981](https://github.com/windmill-labs/windmill/commit/05659816e722effcba27e71f855e819c606f8756))
|
||||
* improve custom component rendering ([4ee4ff7](https://github.com/windmill-labs/windmill/commit/4ee4ff78d389d61c63952c84dee967113c783c45))
|
||||
* improve webhook settings cache invalidation ([0456272](https://github.com/windmill-labs/windmill/commit/0456272e3f36996c5f223fc332b150c7a64c2f05))
|
||||
* update bun t.1.43->1.2.3 ([4e477d1](https://github.com/windmill-labs/windmill/commit/4e477d1f589343980d7bd2953909ff6a6be30739))
|
||||
* update deno 2.1.2->2.2.1 ([b102ff4](https://github.com/windmill-labs/windmill/commit/b102ff4a4643e2f06d44d493f2f776b44ae721cc))
|
||||
|
||||
## [1.467.1](https://github.com/windmill-labs/windmill/compare/v1.467.0...v1.467.1) (2025-02-22)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
ARG DEBIAN_IMAGE=debian:bookworm-slim
|
||||
ARG RUST_IMAGE=rust:1.83-slim-bookworm
|
||||
ARG RUST_IMAGE=rust:1.85-slim-bookworm
|
||||
|
||||
FROM ${RUST_IMAGE} AS rust_base
|
||||
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO job_logs (job_id, logs)\n VALUES ($1, $2)\n ON CONFLICT (job_id) DO UPDATE SET logs = job_logs.logs || '\n' || EXCLUDED.logs\n WHERE job_logs.job_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1ab0d1ba1fbfad31ffb28a01a6c9640d0ac142aabee8d288a4f9c56ad9dbeac4"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow_workspace_runnables SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2bb2cf6accb18d3e37a63388cca52a6591e7593b1a7c3d7a6848587679a48187"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow_workspace_runnables (flow_path, runnable_path, runnable_is_flow, workspace_id) VALUES ($1, $2, TRUE, $3) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "35795d27c4ca69d2f145b4dba08a6ed16c25aea4584103c1b9a3651eb31bfe53"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "CREATE INDEX CONCURRENTLY ix_audit_timestamps ON audit (timestamp DESC)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "47baef9304533898ae52328d946edc053bd3332f886a8bc6cb173cdea7e8aa16"
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH zombie_jobs AS (\n UPDATE v2_job_queue q SET running = false, started_at = null\n FROM v2_job j, v2_job_runtime r\n WHERE j.id = q.id AND j.id = r.id\n AND ping < now() - ($1 || ' seconds')::interval\n AND running = true\n AND kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow')\n AND same_worker = false\n RETURNING q.id, q.workspace_id, ping\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id, workspace_id, ping FROM zombie_jobs",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "ping",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "653574b381a31548d82c1f6f3f44ec826597c42310ab78f9aacd7d9448206c6a"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO job_logs (job_id, logs)\n VALUES ($1, 'Restarted job after not receiving job''s ping for too long the ' || now() || '\n\n')\n ON CONFLICT (job_id) DO UPDATE SET logs = job_logs.logs || '\n' || EXCLUDED.logs\n WHERE job_logs.job_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "67afe352fc26dda9107c90e50e954642d877178ce2c0e73b72c3824135ef86f4"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT f.path\n FROM flow_workspace_runnables fwr \n JOIN flow f \n ON fwr.flow_path = f.path AND fwr.workspace_id = f.workspace_id\n WHERE fwr.runnable_path = $1 AND fwr.runnable_is_flow = $2 AND fwr.workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9aeee333b1dbe58ba819ba3b2713242b54d77b46b5f785ae66b8104f89f43219"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM flow_workspace_runnables WHERE flow_path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acbea8740b28c26942c50edcf5618cd141e68cf83a7dcae7d3c1b8a7ba94425b"
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH to_update AS (\n SELECT q.id, q.workspace_id, r.ping, COALESCE(zjc.counter, 0) as counter\n FROM v2_job_queue q\n JOIN v2_job j ON j.id = q.id\n JOIN v2_job_runtime r ON r.id = j.id\n LEFT JOIN zombie_job_counter zjc ON zjc.job_id = q.id\n WHERE ping < now() - ($1 || ' seconds')::interval\n AND running = true\n AND kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow')\n AND same_worker = false\n AND (zjc.counter IS NULL OR zjc.counter <= $2)\n FOR UPDATE of q SKIP LOCKED\n ),\n zombie_jobs AS (\n UPDATE v2_job_queue q\n SET running = false, started_at = null\n FROM to_update tu\n WHERE q.id = tu.id AND (tu.counter IS NULL OR tu.counter < $2)\n RETURNING q.id, q.workspace_id, ping, tu.counter\n ),\n increment_counter AS (\n INSERT INTO zombie_job_counter (job_id, counter)\n SELECT id, 1 FROM to_update WHERE counter < $2\n ON CONFLICT (job_id) DO UPDATE \n SET counter = zombie_job_counter.counter + 1\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id AS \"id!\", workspace_id AS \"workspace_id!\", ping, counter + 1 AS counter FROM to_update",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "ping",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "counter",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "b45dc2baa48df2272dbac6e3537fc59ad8fab54027a9993cb61cdde68df3cbe6"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow_workspace_runnables SET flow_path = REGEXP_REPLACE(flow_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE flow_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b656927cd70b6667f3c72186ec04f0bf040da3af9e2eac3229264ec95b4755d8"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow_workspace_runnables (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id) VALUES ($1, $2, $3, FALSE, $4) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c35f44f91b08fa57e29a2b4a685706f62e700695810f23108e975dfcd1fee7a3"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow_workspace_runnables SET runnable_path = REGEXP_REPLACE(runnable_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\\1') WHERE runnable_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dc58e5b4715601a93b3c01a2564a4420f232867a23cacb9a62b386f129a86a4b"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id AS \"id!\" FROM v2_job WHERE parent_job = $1 AND workspace_id = $2",
|
||||
"query": "SELECT id AS \"id!\" FROM v2_job_queue INNER JOIN v2_job USING (id) WHERE parent_job = $1 AND v2_job.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -19,5 +19,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c5259e37703c3e48104438bad6e1f3615f4439c090a75e6fde03702a21589b25"
|
||||
"hash": "eb68469026be39048c5f42a80a2c538fbb54ad269ec81aea89e431a511245a1e"
|
||||
}
|
||||
Generated
+629
-819
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.467.1"
|
||||
version = "1.473.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.467.1"
|
||||
version = "1.473.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -59,7 +59,7 @@ flow_testing = ["windmill-worker/flow_testing"]
|
||||
openidconnect = ["windmill-api/openidconnect"]
|
||||
cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
|
||||
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
|
||||
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "enterprise", "parquet"]
|
||||
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "windmill-common/tantivy", "enterprise", "parquet"]
|
||||
sqlx = ["windmill-worker/sqlx"]
|
||||
deno_core = ["windmill-worker/deno_core", "dep:deno_core", "dep:v8"]
|
||||
kafka = ["windmill-api/kafka"]
|
||||
@@ -173,7 +173,7 @@ serde_json = { version = "^1", features = ["preserve_order", "raw_value"] }
|
||||
uuid = { version = "^1", features = ["serde", "v4"] }
|
||||
thiserror = "^2"
|
||||
anyhow = "^1"
|
||||
chrono = { version = "0.4.35", features = ["serde"] }
|
||||
chrono = { version = "=0.4.39", features = ["serde"] }
|
||||
chrono-tz = "^0.10.1"
|
||||
tracing = "^0"
|
||||
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
|
||||
@@ -211,20 +211,20 @@ itertools = "^0"
|
||||
regex = "^1"
|
||||
semver = "^1"
|
||||
|
||||
v8 = "=134.4.0" # Exact version
|
||||
deno_fetch = "0.216.0"
|
||||
deno_tls = "0.179.0"
|
||||
deno_console = "0.192.0"
|
||||
deno_url = "0.192.0"
|
||||
deno_webidl = "0.192.0"
|
||||
deno_web = "0.223.0"
|
||||
deno_io = "0.102.0"
|
||||
deno_net = "0.184.0"
|
||||
deno_core = "0.338.0"
|
||||
v8 = "=130.0.7" # Exact version
|
||||
deno_fetch = "0.214.0"
|
||||
deno_tls = "0.177.0"
|
||||
deno_console = "0.190.0"
|
||||
deno_url = "0.190.0"
|
||||
deno_webidl = "0.190.0"
|
||||
deno_web = "0.221.0"
|
||||
deno_io = "0.100.0"
|
||||
deno_net = "0.182.0"
|
||||
deno_core = "0.336.0"
|
||||
deno_ast = { version = "=0.44.0", features = ["transpiling"] }
|
||||
deno_permissions = "0.51.0"
|
||||
deno_runtime = { version = "0.200.0", features = ["transpile"] }
|
||||
deno_telemetry = "0.14.0"
|
||||
deno_permissions = "0.49.0"
|
||||
deno_runtime = { version = "0.198.0", features = ["transpile"] }
|
||||
deno_telemetry = "0.12.0"
|
||||
deno_error = "=0.5.5"
|
||||
|
||||
swc_common = "=0.37.5"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
DO
|
||||
$$
|
||||
DECLARE
|
||||
tbl_name text;
|
||||
policy_exists boolean;
|
||||
current_sch text;
|
||||
tbl_names text[] := ARRAY['account', 'app', 'audit', 'capture', 'completed_job', 'flow', 'folder', 'http_trigger', 'queue', 'raw_app', 'resource', 'schedule', 'script', 'usr_to_group', 'variable'];
|
||||
BEGIN
|
||||
-- Get the current schema
|
||||
SELECT current_schema() INTO current_sch;
|
||||
|
||||
FOR tbl_name IN SELECT unnest(tbl_names)
|
||||
LOOP
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_policies
|
||||
WHERE schemaname = current_sch
|
||||
AND tablename = tbl_name
|
||||
AND policyname = 'admin_policy'
|
||||
) INTO policy_exists;
|
||||
|
||||
IF NOT policy_exists THEN
|
||||
EXECUTE format('CREATE POLICY admin_policy ON %I.%I TO windmill_admin USING (true);', current_sch, tbl_name);
|
||||
END IF;
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
@@ -1 +1 @@
|
||||
2dca712cde013e7dda96dc3162e47a148973a0dc
|
||||
a165bb74c6c33bdcf618d564498fd4cbb5742451
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add down migration script here
|
||||
|
||||
DROP TRIGGER webhook_change_trigger ON workspace_settings;
|
||||
DROP FUNCTION notify_webhook_change();
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Add up migration script here
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_webhook_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('notify_webhook_change', NEW.workspace_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER webhook_change_trigger
|
||||
AFTER UPDATE OF webhook ON workspace_settings
|
||||
FOR EACH ROW
|
||||
WHEN (OLD.webhook IS DISTINCT FROM NEW.webhook)
|
||||
EXECUTE FUNCTION notify_webhook_change();
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
|
||||
DROP TABLE zombie_job_counter;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Add up migration script here
|
||||
|
||||
CREATE TABLE IF NOT EXISTS zombie_job_counter (
|
||||
job_id UUID PRIMARY KEY REFERENCES v2_job (id) ON DELETE CASCADE,
|
||||
counter INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,37 @@
|
||||
DO
|
||||
$do$
|
||||
DECLARE
|
||||
current_schema_name TEXT;
|
||||
BEGIN
|
||||
-- Get the current schema for the session
|
||||
SELECT current_schema() INTO current_schema_name;
|
||||
|
||||
-- Lock the roles table to prevent race conditions
|
||||
LOCK TABLE pg_catalog.pg_roles;
|
||||
|
||||
|
||||
|
||||
EXECUTE format('GRANT USAGE ON SCHEMA %I TO windmill_user', current_schema_name);
|
||||
EXECUTE format('GRANT USAGE ON SCHEMA %I TO windmill_admin', current_schema_name);
|
||||
|
||||
|
||||
-- Grant privileges dynamically to the current schema
|
||||
EXECUTE format('GRANT ALL ON ALL TABLES IN SCHEMA %I TO windmill_user', current_schema_name);
|
||||
EXECUTE format('GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %I TO windmill_user', current_schema_name);
|
||||
|
||||
-- Alter default privileges dynamically
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON TABLES TO windmill_user', current_schema_name);
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON SEQUENCES TO windmill_user', current_schema_name);
|
||||
|
||||
-- Grant privileges dynamically to the current schema
|
||||
EXECUTE format('GRANT ALL ON ALL TABLES IN SCHEMA %I TO windmill_admin', current_schema_name);
|
||||
EXECUTE format('GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA %I TO windmill_admin', current_schema_name);
|
||||
|
||||
-- Alter default privileges dynamically
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON TABLES TO windmill_admin', current_schema_name);
|
||||
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT ALL ON SEQUENCES TO windmill_admin', current_schema_name);
|
||||
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'Error granting proper permissions to windmill users: %', SQLERRM;
|
||||
END
|
||||
$do$;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE flow_workspace_runnables;
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE flow_workspace_runnables (
|
||||
flow_path VARCHAR(255) NOT NULL,
|
||||
runnable_path VARCHAR(255) NOT NULL,
|
||||
script_hash BIGINT NULL,
|
||||
runnable_is_flow BOOLEAN NOT NULL,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
FOREIGN KEY (workspace_id, flow_path) REFERENCES flow (workspace_id, path) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX flow_workspace_without_hash_unique_idx ON flow_workspace_runnables (flow_path, runnable_path, runnable_is_flow, workspace_id) WHERE script_hash IS NULL;
|
||||
CREATE UNIQUE INDEX flow_workspace_with_hash_unique_idx ON flow_workspace_runnables (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id) WHERE script_hash IS NOT NULL;
|
||||
CREATE INDEX flow_workspace_runnable_path_is_flow_idx ON flow_workspace_runnables (runnable_path, runnable_is_flow, workspace_id);
|
||||
@@ -96,11 +96,11 @@ pub fn parse_python_signature(
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, x)| {
|
||||
let mut typ = x
|
||||
let (mut typ, has_default) = x
|
||||
.as_arg()
|
||||
.annotation
|
||||
.as_ref()
|
||||
.map_or(Typ::Unknown, |e| parse_expr(e));
|
||||
.map_or((Typ::Unknown, false), |e| parse_expr(e));
|
||||
|
||||
let default = if i >= def_arg_start {
|
||||
params
|
||||
@@ -138,7 +138,7 @@ pub fn parse_python_signature(
|
||||
otyp: None,
|
||||
name: x.as_arg().arg.to_string(),
|
||||
typ,
|
||||
has_default: default.is_some(),
|
||||
has_default: has_default || default.is_some(),
|
||||
default,
|
||||
oidx: None,
|
||||
}
|
||||
@@ -158,17 +158,27 @@ pub fn parse_python_signature(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_expr(e: &Box<Expr>) -> Typ {
|
||||
fn parse_expr(e: &Box<Expr>) -> (Typ, bool) {
|
||||
match e.as_ref() {
|
||||
Expr::Name(ExprName { id, .. }) => parse_typ(id.as_ref()),
|
||||
Expr::Name(ExprName { id, .. }) => (parse_typ(id.as_ref()), false),
|
||||
Expr::Attribute(x) => {
|
||||
if x.value
|
||||
.as_name_expr()
|
||||
.is_some_and(|x| x.id.as_str() == "wmill")
|
||||
{
|
||||
parse_typ(x.attr.as_str())
|
||||
(parse_typ(x.attr.as_str()), false)
|
||||
} else {
|
||||
Typ::Unknown
|
||||
(Typ::Unknown, false)
|
||||
}
|
||||
}
|
||||
Expr::BinOp(x) => {
|
||||
if matches!(
|
||||
x.right.as_ref(),
|
||||
Expr::Constant(ExprConstant { value: Constant::None, .. })
|
||||
) {
|
||||
(parse_expr(&x.left).0, true)
|
||||
} else {
|
||||
(Typ::Unknown, false)
|
||||
}
|
||||
}
|
||||
Expr::Subscript(x) => match x.value.as_ref() {
|
||||
@@ -193,14 +203,15 @@ fn parse_expr(e: &Box<Expr>) -> Typ {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Typ::Str(values)
|
||||
(Typ::Str(values), false)
|
||||
}
|
||||
"List" => Typ::List(Box::new(parse_expr(&x.slice))),
|
||||
_ => Typ::Unknown,
|
||||
"List" => (Typ::List(Box::new(parse_expr(&x.slice).0)), false),
|
||||
"Optional" => (parse_expr(&x.slice).0, true),
|
||||
_ => (Typ::Unknown, false),
|
||||
},
|
||||
_ => Typ::Unknown,
|
||||
_ => (Typ::Unknown, false),
|
||||
},
|
||||
_ => Typ::Unknown,
|
||||
_ => (Typ::Unknown, false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,4 +687,53 @@ def main(a: list, e: List[int], b: list = [1,2,3,4], c = [1,2,3,4], d = ["a", "b
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_python_sig_9() -> anyhow::Result<()> {
|
||||
let code = r#"
|
||||
from typing import Optional
|
||||
def main(a: str, b: Optional[str], c: str | None): return
|
||||
"#;
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&parse_python_signature(code, None, false)?)?
|
||||
);
|
||||
assert_eq!(
|
||||
parse_python_signature(code, None, false)?,
|
||||
MainArgSignature {
|
||||
star_args: false,
|
||||
star_kwargs: false,
|
||||
args: vec![
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "a".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false,
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "b".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: true,
|
||||
oidx: None
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "c".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: true,
|
||||
oidx: None
|
||||
},
|
||||
],
|
||||
no_main_func: Some(false),
|
||||
has_preprocessor: Some(false)
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
|
||||
use windmill_parser_bash::parse_powershell_sig;
|
||||
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_ids, parse_expr_for_imports};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_deno_sig() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -146,6 +147,8 @@ export function main(test1?: string, test2: string = \"burkina\",
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_deno_sig_implicit_types() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -224,6 +227,7 @@ export function main(test2 = \"burkina\",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_deno_types() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -274,6 +278,7 @@ export function main(foo: FooBar, {a, b}: FooBar, {c, d}: FooBar = {a: \"foo\",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_enum_list() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -305,6 +310,7 @@ export function main(foo: (\"foo\" | \"bar\")[]) {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_extract_ident() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -324,6 +330,7 @@ fn test_parse_extract_ident() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_imports() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -347,6 +354,7 @@ fn test_parse_imports() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_imports_dts() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
@@ -359,6 +367,7 @@ export type foo = number
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen_test]
|
||||
fn test_parse_powershell_sig() -> anyhow::Result<()> {
|
||||
let code = "
|
||||
|
||||
+40
-61
@@ -44,7 +44,7 @@ use windmill_common::{
|
||||
},
|
||||
scripts::ScriptLang,
|
||||
stats_ee::schedule_stats,
|
||||
utils::{hostname, rd_string, Mode, GIT_VERSION},
|
||||
utils::{hostname, rd_string, Mode, GIT_VERSION, MODE_AND_ADDONS},
|
||||
worker::{reload_custom_tags_setting, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP},
|
||||
DB, METRICS_ENABLED,
|
||||
};
|
||||
@@ -115,6 +115,25 @@ where
|
||||
}
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
// https://github.com/denoland/deno/blob/main/cli/main.rs#L477
|
||||
#[cfg(feature = "deno_core")]
|
||||
let unrecognized_v8_flags = deno_core::v8_set_flags(vec![
|
||||
"--stack-size=1024".to_string(),
|
||||
// TODO(bartlomieju): I think this can be removed as it's handled by `deno_core`
|
||||
// and its settings.
|
||||
// deno_ast removes TypeScript `assert` keywords, so this flag only affects JavaScript
|
||||
// TODO(petamoriken): Need to check TypeScript `assert` keywords in deno_ast
|
||||
"--no-harmony-import-assertions".to_string(),
|
||||
])
|
||||
.into_iter()
|
||||
.skip(1)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
if !unrecognized_v8_flags.is_empty() {
|
||||
println!("Unrecognized V8 flags: {:?}", unrecognized_v8_flags);
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
deno_core::JsRuntime::init_platform(None, false);
|
||||
create_and_run_current_thread_inner(windmill_main())
|
||||
@@ -219,63 +238,12 @@ async fn windmill_main() -> anyhow::Result<()> {
|
||||
|
||||
let hostname = hostname();
|
||||
|
||||
let mut enable_standalone_indexer: bool = false;
|
||||
let mode_and_addons = MODE_AND_ADDONS.clone();
|
||||
let mode = mode_and_addons.mode;
|
||||
|
||||
let mode = std::env::var("MODE")
|
||||
.map(|x| x.to_lowercase())
|
||||
.map(|x| {
|
||||
if &x == "server" {
|
||||
println!("Binary is in 'server' mode");
|
||||
Mode::Server
|
||||
} else if &x == "worker" {
|
||||
tracing::info!("Binary is in 'worker' mode");
|
||||
#[cfg(windows)]
|
||||
{
|
||||
println!("It is highly recommended to use the agent mode instead on windows (MODE=agent) and to pass a BASE_INTERNAL_URL");
|
||||
}
|
||||
Mode::Worker
|
||||
} else if &x == "agent" {
|
||||
println!("Binary is in 'agent' mode");
|
||||
if std::env::var("BASE_INTERNAL_URL").is_err() {
|
||||
panic!("BASE_INTERNAL_URL is required in agent mode")
|
||||
}
|
||||
if std::env::var("JOB_TOKEN").is_err() {
|
||||
println!("JOB_TOKEN is not passed, hence workers will still need to create permissions for each job and the DATABASE_URL needs to be of a role that can INSERT into the job_perms table")
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
panic!("Agent mode is only available in the EE, ignoring...");
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
Mode::Agent
|
||||
} else if &x == "indexer" {
|
||||
tracing::info!("Binary is in 'indexer' mode");
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
{
|
||||
eprintln!("Cannot start the indexer because tantivy is not included in this binary/image. Make sure you are using the EE image if you want to access the full text search features.");
|
||||
panic!("Indexer mode requires compiling with the tantivy feature flag.");
|
||||
}
|
||||
#[cfg(feature = "tantivy")]
|
||||
Mode::Indexer
|
||||
} else if &x == "standalone+search"{
|
||||
enable_standalone_indexer = true;
|
||||
println!("Binary is in 'standalone' mode with search enabled");
|
||||
Mode::Standalone
|
||||
}
|
||||
else {
|
||||
if &x != "standalone" {
|
||||
eprintln!("mode not recognized, defaulting to standalone: {x}");
|
||||
} else {
|
||||
println!("Binary is in 'standalone' mode");
|
||||
}
|
||||
Mode::Standalone
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
tracing::info!("Mode not specified, defaulting to standalone");
|
||||
Mode::Standalone
|
||||
});
|
||||
if mode == Mode::Standalone {
|
||||
println!("Running in standalone mode");
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
|
||||
println!("jemalloc enabled");
|
||||
@@ -518,8 +486,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
.expect("could not create initial server dir");
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
let should_index_jobs =
|
||||
mode == Mode::Indexer || (enable_standalone_indexer && mode == Mode::Standalone);
|
||||
let should_index_jobs = mode == Mode::Indexer || mode_and_addons.indexer;
|
||||
|
||||
reload_indexer_config(&db).await;
|
||||
|
||||
@@ -722,6 +689,11 @@ Windmill Community Edition {GIT_VERSION}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notify_webhook_change" => {
|
||||
let workspace_id = n.payload();
|
||||
tracing::info!("Webhook change detected, invalidating webhook cache: {}", workspace_id);
|
||||
windmill_api::webhook_util::WEBHOOK_CACHE.remove(workspace_id);
|
||||
},
|
||||
"notify_global_setting_change" => {
|
||||
tracing::info!("Global setting change detected: {}", n.payload());
|
||||
match n.payload() {
|
||||
@@ -960,7 +932,11 @@ async fn listen_pg(db: &DB) -> Option<PgListener> {
|
||||
};
|
||||
|
||||
if let Err(e) = listener
|
||||
.listen_all(vec!["notify_config_change", "notify_global_setting_change"])
|
||||
.listen_all(vec![
|
||||
"notify_config_change",
|
||||
"notify_global_setting_change",
|
||||
"notify_webhook_change",
|
||||
])
|
||||
.await
|
||||
{
|
||||
tracing::error!(error = %e, "Could not listen to database");
|
||||
@@ -1119,9 +1095,12 @@ pub async fn run_workers(
|
||||
|
||||
async fn send_delayed_killpill(
|
||||
tx: &tokio::sync::broadcast::Sender<()>,
|
||||
max_delay_secs: u64,
|
||||
mut max_delay_secs: u64,
|
||||
context: &str,
|
||||
) {
|
||||
if max_delay_secs == 0 {
|
||||
max_delay_secs = 1;
|
||||
}
|
||||
// Random delay to avoid all servers/workers shutting down simultaneously
|
||||
let rd_delay = rand::rng().random_range(0..max_delay_secs);
|
||||
tracing::info!("Scheduling {context} shutdown in {rd_delay}s");
|
||||
|
||||
+122
-27
@@ -34,7 +34,6 @@ use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
|
||||
#[cfg(feature = "oauth2")]
|
||||
use windmill_common::global_settings::OAUTH_SETTING;
|
||||
use windmill_common::{
|
||||
auth::JWT_SECRET,
|
||||
ee::CriticalErrorChannel,
|
||||
error,
|
||||
flow_status::{FlowStatus, FlowStatusModule},
|
||||
@@ -51,6 +50,7 @@ use windmill_common::{
|
||||
},
|
||||
indexer::load_indexer_config,
|
||||
jobs::QueuedJob,
|
||||
jwt::JWT_SECRET,
|
||||
oauth2::REQUIRE_PREEXISTING_USER_FOR_OAUTH,
|
||||
server::load_smtp_config,
|
||||
tracing_init::JSON_FMT,
|
||||
@@ -1533,18 +1533,38 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const RESTART_LIMIT: i32 = 3;
|
||||
|
||||
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
|
||||
let mut zombie_jobs_uuid_restart_limit_reached = vec![];
|
||||
|
||||
if *RESTART_ZOMBIE_JOBS {
|
||||
let restarted = sqlx::query!(
|
||||
"WITH zombie_jobs AS (
|
||||
UPDATE v2_job_queue q SET running = false, started_at = null
|
||||
FROM v2_job j, v2_job_runtime r
|
||||
WHERE j.id = q.id AND j.id = r.id
|
||||
AND ping < now() - ($1 || ' seconds')::interval
|
||||
"WITH to_update AS (
|
||||
SELECT q.id, q.workspace_id, r.ping, COALESCE(zjc.counter, 0) as counter
|
||||
FROM v2_job_queue q
|
||||
JOIN v2_job j ON j.id = q.id
|
||||
JOIN v2_job_runtime r ON r.id = j.id
|
||||
LEFT JOIN zombie_job_counter zjc ON zjc.job_id = q.id
|
||||
WHERE ping < now() - ($1 || ' seconds')::interval
|
||||
AND running = true
|
||||
AND kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow')
|
||||
AND same_worker = false
|
||||
RETURNING q.id, q.workspace_id, ping
|
||||
AND (zjc.counter IS NULL OR zjc.counter <= $2)
|
||||
FOR UPDATE of q SKIP LOCKED
|
||||
),
|
||||
zombie_jobs AS (
|
||||
UPDATE v2_job_queue q
|
||||
SET running = false, started_at = null
|
||||
FROM to_update tu
|
||||
WHERE q.id = tu.id AND (tu.counter IS NULL OR tu.counter < $2)
|
||||
RETURNING q.id, q.workspace_id, ping, tu.counter
|
||||
),
|
||||
increment_counter AS (
|
||||
INSERT INTO zombie_job_counter (job_id, counter)
|
||||
SELECT id, 1 FROM to_update WHERE counter < $2
|
||||
ON CONFLICT (job_id) DO UPDATE
|
||||
SET counter = zombie_job_counter.counter + 1
|
||||
),
|
||||
update_concurrency AS (
|
||||
UPDATE concurrency_counter cc
|
||||
@@ -1553,8 +1573,9 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
INNER JOIN concurrency_key ck ON ck.job_id = zj.id
|
||||
WHERE cc.concurrency_id = ck.key
|
||||
)
|
||||
SELECT id, workspace_id, ping FROM zombie_jobs",
|
||||
SELECT id AS \"id!\", workspace_id AS \"workspace_id!\", ping, counter + 1 AS counter FROM to_update",
|
||||
*ZOMBIE_JOB_TIMEOUT,
|
||||
RESTART_LIMIT
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@@ -1574,22 +1595,61 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
"no last ping".to_string()
|
||||
};
|
||||
let url = format!("{}/run/{}?workspace={}", base_url, r.id, r.workspace_id,);
|
||||
let error_message = format!(
|
||||
"Zombie job {} on {} ({}) detected, restarting it, {}",
|
||||
r.id, r.workspace_id, url, last_ping
|
||||
);
|
||||
let restart = r.counter.is_none_or(|x| x < RESTART_LIMIT);
|
||||
let (critical_error_message, restart_message) = if restart {
|
||||
(
|
||||
format!(
|
||||
"Zombie job {} on {} ({}) detected, restarting it ({}/{} attempts), last ping: {}",
|
||||
r.id,
|
||||
r.workspace_id,
|
||||
url,
|
||||
r.counter.unwrap_or(0) + 1,
|
||||
RESTART_LIMIT,
|
||||
last_ping
|
||||
),
|
||||
format!(
|
||||
"Restarted job after not receiving job's ping for too long the {} ({}/{} attempts)\n\n",
|
||||
last_ping,
|
||||
r.counter.unwrap_or(0) + 1,
|
||||
RESTART_LIMIT
|
||||
)
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!(
|
||||
"Zombie job {} on {} ({}) detected, but restart limit ({}) reached, job will be processed as an error, last ping: {}",
|
||||
r.id, r.workspace_id, url, RESTART_LIMIT, last_ping
|
||||
),
|
||||
format!(
|
||||
"job's ping was received last at {}, job will be processed as an error since all {} restart attempts failed",
|
||||
last_ping, RESTART_LIMIT
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
let _ = sqlx::query!("
|
||||
let _ = sqlx::query!(
|
||||
"
|
||||
INSERT INTO job_logs (job_id, logs)
|
||||
VALUES ($1, 'Restarted job after not receiving job''s ping for too long the ' || now() || '\n\n')
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (job_id) DO UPDATE SET logs = job_logs.logs || '\n' || EXCLUDED.logs
|
||||
WHERE job_logs.job_id = $1",
|
||||
r.id
|
||||
r.id,
|
||||
restart_message
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
tracing::error!(error_message);
|
||||
report_critical_error(error_message, db.clone(), Some(&r.workspace_id), None).await;
|
||||
tracing::error!(critical_error_message);
|
||||
report_critical_error(
|
||||
critical_error_message,
|
||||
db.clone(),
|
||||
Some(&r.workspace_id),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
if !restart {
|
||||
zombie_jobs_uuid_restart_limit_reached.push(r.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1664,9 +1724,43 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
.unwrap_or_else(|| vec![])
|
||||
};
|
||||
|
||||
enum ErrorMessage {
|
||||
RestartLimit,
|
||||
SameWorker,
|
||||
RestartDisabled,
|
||||
}
|
||||
|
||||
impl ErrorMessage {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
ErrorMessage::RestartLimit => format!("RestartLimit ({})", RESTART_LIMIT),
|
||||
ErrorMessage::SameWorker => "SameWorker".to_string(),
|
||||
ErrorMessage::RestartDisabled => "RestartDisabled".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zombie_jobs_restart_limit_reached =
|
||||
sqlx::query_as::<_, QueuedJob>("SELECT * FROM v2_as_queue WHERE id = ANY($1)")
|
||||
.bind(&zombie_jobs_uuid_restart_limit_reached[..])
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_else(|| vec![]);
|
||||
|
||||
let timeouts = non_restartable_jobs
|
||||
.into_iter()
|
||||
.chain(same_worker_timeout_jobs)
|
||||
.map(|x| (x, ErrorMessage::RestartDisabled))
|
||||
.chain(
|
||||
same_worker_timeout_jobs
|
||||
.into_iter()
|
||||
.map(|x| (x, ErrorMessage::SameWorker)),
|
||||
)
|
||||
.chain(
|
||||
zombie_jobs_restart_limit_reached
|
||||
.into_iter()
|
||||
.map(|x| (x, ErrorMessage::RestartLimit)),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
@@ -1674,7 +1768,7 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
QUEUE_ZOMBIE_DELETE_COUNT.inc_by(timeouts.len() as _);
|
||||
}
|
||||
|
||||
for job in timeouts {
|
||||
for (job, error_kind) in timeouts {
|
||||
// since the job is unrecoverable, the same worker queue should never be sent anything
|
||||
let (same_worker_tx_never_used, _same_worker_rx_never_used) =
|
||||
mpsc::channel::<SameWorkerPayload>(1);
|
||||
@@ -1709,20 +1803,21 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker
|
||||
};
|
||||
|
||||
let last_ping = job.last_ping.clone();
|
||||
let error_message = format!(
|
||||
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?})",
|
||||
last_ping
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "no ping".to_string()),
|
||||
*ZOMBIE_JOB_TIMEOUT,
|
||||
error_kind.to_string()
|
||||
);
|
||||
let _ = handle_job_error(
|
||||
db,
|
||||
&client,
|
||||
&job,
|
||||
0,
|
||||
None,
|
||||
error::Error::ExecutionErr(format!(
|
||||
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, same_worker: {})",
|
||||
last_ping
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "no ping".to_string()),
|
||||
*ZOMBIE_JOB_TIMEOUT,
|
||||
job.same_worker
|
||||
)),
|
||||
error::Error::ExecutionErr(error_message),
|
||||
true,
|
||||
same_worker_tx_never_used,
|
||||
"",
|
||||
|
||||
@@ -22,12 +22,12 @@ use windmill_api_client::types::{CreateFlowBody, RawScript};
|
||||
use windmill_api_client::types::{EditSchedule, NewSchedule, ScriptArgs};
|
||||
|
||||
use serde::Serialize;
|
||||
use windmill_common::auth::JWT_SECRET;
|
||||
use windmill_common::worker::WORKER_CONFIG;
|
||||
use windmill_common::{
|
||||
flow_status::{FlowStatus, FlowStatusModule, RestartedFrom},
|
||||
flows::{FlowModule, FlowModuleValue, FlowValue, InputTransform},
|
||||
jobs::{JobKind, JobPayload, RawCode},
|
||||
jwt::JWT_SECRET,
|
||||
scripts::{ScriptHash, ScriptLang},
|
||||
worker::{
|
||||
MIN_VERSION_IS_AT_LEAST_1_427, MIN_VERSION_IS_AT_LEAST_1_432, MIN_VERSION_IS_AT_LEAST_1_440,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.467.1
|
||||
version: 1.473.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -126,6 +126,11 @@ paths:
|
||||
type: string
|
||||
- $ref: "#/components/parameters/ResourceName"
|
||||
- $ref: "#/components/parameters/ActionKind"
|
||||
- name: all_workspaces
|
||||
in: query
|
||||
description: get audit logs for all workspaces
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
responses:
|
||||
"200":
|
||||
@@ -5287,6 +5292,26 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FlowVersion"
|
||||
|
||||
/w/{workspace}/flows/list_paths_from_workspace_runnable/{runnable_kind}/{path}:
|
||||
get:
|
||||
summary: list flow paths from workspace runnable
|
||||
operationId: listFlowPathsFromWorkspaceRunnable
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/RunnableKind"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: list of flow paths
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/flows/get/v/{version}/p/{path}:
|
||||
get:
|
||||
summary: get flow version
|
||||
@@ -6207,6 +6232,92 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/apps_u/upload_s3_file/{path}:
|
||||
post:
|
||||
summary: upload s3 file from app
|
||||
operationId: uploadS3FileFromApp
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
- name: file_key
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: file_extension
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: s3_resource_path
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: resource_type
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: storage
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: content_type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: content_disposition
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: File content
|
||||
required: true
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
description: file uploaded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file_key:
|
||||
type: string
|
||||
delete_token:
|
||||
type: string
|
||||
required:
|
||||
- file_key
|
||||
- delete_token
|
||||
|
||||
/w/{workspace}/apps_u/delete_s3_file:
|
||||
delete:
|
||||
summary: delete s3 file from app
|
||||
operationId: deleteS3FileFromApp
|
||||
tags:
|
||||
- app
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- name: delete_token
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: file deleted
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs/run/f/{path}:
|
||||
post:
|
||||
summary: run flow by path
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
job_helpers_ee::{
|
||||
download_s3_file_internal, get_random_file_name, get_s3_resource,
|
||||
get_workspace_s3_resource, load_image_preview_internal, upload_file_from_req,
|
||||
DownloadFileQuery, LoadImagePreviewQuery, UploadFileResponse,
|
||||
DownloadFileQuery, LoadImagePreviewQuery,
|
||||
},
|
||||
users::fetch_api_authed_from_permissioned_as,
|
||||
};
|
||||
@@ -51,8 +51,6 @@ use sqlx::{types::Uuid, FromRow};
|
||||
use std::str;
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::s3_helpers::build_object_store_client;
|
||||
use windmill_common::variables::encrypt;
|
||||
use windmill_common::{
|
||||
apps::{AppScriptId, ListAppQuery},
|
||||
@@ -69,6 +67,8 @@ use windmill_common::{
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
HUB_BASE_URL,
|
||||
};
|
||||
#[cfg(feature = "parquet")]
|
||||
use windmill_common::{jwt, s3_helpers::build_object_store_client};
|
||||
|
||||
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
|
||||
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel};
|
||||
@@ -96,6 +96,7 @@ pub fn unauthed_service() -> Router {
|
||||
Router::new()
|
||||
.route("/execute_component/*path", post(execute_component))
|
||||
.route("/upload_s3_file/*path", post(upload_s3_file_from_app))
|
||||
.route("/delete_s3_file", delete(delete_s3_file_from_app))
|
||||
.route("/download_s3_file/*path", get(download_s3_file_from_app))
|
||||
.route(
|
||||
"/load_image_preview/*path",
|
||||
@@ -1497,6 +1498,13 @@ async fn upload_s3_file_from_app() -> Result<()> {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
async fn delete_s3_file_from_app() -> Result<()> {
|
||||
return Err(Error::BadRequest(
|
||||
"This endpoint requires the parquet feature to be enabled".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
struct UploadFileToS3Query {
|
||||
@@ -1511,6 +1519,24 @@ struct UploadFileToS3Query {
|
||||
force_viewer_allowed_resources: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DeleteTokenClaims {
|
||||
file_key: String,
|
||||
on_behalf_of_email: String,
|
||||
permissioned_as: String,
|
||||
username: String,
|
||||
s3_resource_path: Option<String>,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Serialize)]
|
||||
struct AppUploadFileResponse {
|
||||
file_key: String,
|
||||
delete_token: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn upload_s3_file_from_app(
|
||||
OptAuthed(opt_authed): OptAuthed,
|
||||
@@ -1518,7 +1544,7 @@ async fn upload_s3_file_from_app(
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(query): Query<UploadFileToS3Query>,
|
||||
request: axum::extract::Request,
|
||||
) -> JsonResult<UploadFileResponse> {
|
||||
) -> JsonResult<AppUploadFileResponse> {
|
||||
let policy = if let Some(file_key_regex) = query.force_viewer_file_key_regex {
|
||||
Some(Policy {
|
||||
execution_mode: ExecutionMode::Viewer,
|
||||
@@ -1554,7 +1580,10 @@ async fn upload_s3_file_from_app(
|
||||
|
||||
let user_db = UserDB::new(db.clone());
|
||||
|
||||
let (s3_resource_opt, file_key) = if policy.as_ref().is_some_and(|p| p.s3_inputs.is_some()) {
|
||||
let (s3_resource_opt, file_key, on_behalf_of_email, permissioned_as, username) = if policy
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.s3_inputs.is_some())
|
||||
{
|
||||
let policy = policy.unwrap();
|
||||
let s3_inputs = policy.s3_inputs.as_ref().unwrap();
|
||||
|
||||
@@ -1562,11 +1591,11 @@ async fn upload_s3_file_from_app(
|
||||
get_on_behalf_details_from_policy_and_authed(&policy, &opt_authed).await?;
|
||||
|
||||
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
|
||||
permissioned_as,
|
||||
email,
|
||||
permissioned_as.clone(),
|
||||
email.clone(),
|
||||
&w_id,
|
||||
&db,
|
||||
Some(username),
|
||||
Some(username.clone()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1617,6 +1646,9 @@ async fn upload_s3_file_from_app(
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -1640,13 +1672,16 @@ async fn upload_s3_file_from_app(
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None)
|
||||
.await?;
|
||||
(s3_resource_opt, file_key)
|
||||
(s3_resource_opt, file_key, email, permissioned_as, username)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -1672,7 +1707,7 @@ async fn upload_s3_file_from_app(
|
||||
let (_, s3_resource_opt) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource_opt, file_key)
|
||||
(s3_resource_opt, file_key, email, permissioned_as, username)
|
||||
}
|
||||
} else {
|
||||
// backward compatibility (no policy)
|
||||
@@ -1682,6 +1717,12 @@ async fn upload_s3_file_from_app(
|
||||
.file_key
|
||||
.unwrap_or_else(|| get_random_file_name(query.file_extension));
|
||||
|
||||
let (on_behalf_of_email, permissioned_as, username) = (
|
||||
authed.email.clone(),
|
||||
username_to_permissioned_as(&authed.username),
|
||||
authed.display_username().to_string(),
|
||||
);
|
||||
|
||||
if let Some(ref s3_resource_path) = query.s3_resource_path {
|
||||
(
|
||||
Some(
|
||||
@@ -1698,12 +1739,21 @@ async fn upload_s3_file_from_app(
|
||||
.await?,
|
||||
),
|
||||
file_key,
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
} else {
|
||||
let (_, s3_resource) =
|
||||
get_workspace_s3_resource(&authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
(s3_resource, file_key)
|
||||
(
|
||||
s3_resource,
|
||||
file_key,
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return Err(Error::BadRequest("Missing s3 policy".to_string()));
|
||||
@@ -1733,7 +1783,81 @@ async fn upload_s3_file_from_app(
|
||||
|
||||
upload_file_from_req(s3_client, &file_key, request, options).await?;
|
||||
|
||||
return Ok(Json(UploadFileResponse { file_key }));
|
||||
let delete_token = jwt::encode_with_internal_secret(DeleteTokenClaims {
|
||||
file_key: file_key.clone(),
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
s3_resource_path: query.s3_resource_path,
|
||||
exp: (chrono::Utc::now() + chrono::Duration::seconds(3600 * 24)).timestamp() as usize,
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(Json(AppUploadFileResponse { file_key, delete_token }));
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
#[derive(Deserialize)]
|
||||
struct DeleteS3FileQuery {
|
||||
delete_token: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
async fn delete_s3_file_from_app(
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(query): Query<DeleteS3FileQuery>,
|
||||
) -> Result<()> {
|
||||
let DeleteTokenClaims {
|
||||
file_key,
|
||||
on_behalf_of_email,
|
||||
permissioned_as,
|
||||
username,
|
||||
s3_resource_path,
|
||||
..
|
||||
} = jwt::decode_with_internal_secret::<DeleteTokenClaims>(&query.delete_token).await?;
|
||||
|
||||
let on_behalf_authed = fetch_api_authed_from_permissioned_as(
|
||||
permissioned_as,
|
||||
on_behalf_of_email,
|
||||
&w_id,
|
||||
&db,
|
||||
Some(username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let s3_resource = if let Some(s3_resource_path) = s3_resource_path {
|
||||
get_s3_resource(
|
||||
&on_behalf_authed,
|
||||
&db,
|
||||
Some(user_db),
|
||||
"",
|
||||
&w_id,
|
||||
s3_resource_path.as_str(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
let (_, s3_resource) =
|
||||
get_workspace_s3_resource(&on_behalf_authed, &db, None, "", &w_id, None).await?;
|
||||
|
||||
s3_resource.ok_or(Error::internal_err(
|
||||
"No files storage resource defined at the workspace level".to_string(),
|
||||
))?
|
||||
};
|
||||
|
||||
let s3_client = build_object_store_client(&s3_resource).await?;
|
||||
|
||||
let path = object_store::path::Path::from(file_key.as_str());
|
||||
|
||||
s3_client.delete(&path).await.map_err(|err| {
|
||||
tracing::error!("Error deleting file: {:?}", err);
|
||||
Error::internal_err(format!("Error deleting file: {}", err.to_string()))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parquet"))]
|
||||
|
||||
@@ -21,7 +21,8 @@ use std::sync::{
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use windmill_common::{
|
||||
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims, JWT_SECRET},
|
||||
auth::{get_folders_for_user, get_groups_for_user, JWTAuthClaims},
|
||||
jwt,
|
||||
users::{COOKIE_NAME, SUPERADMIN_SECRET_EMAIL},
|
||||
};
|
||||
|
||||
@@ -99,55 +100,44 @@ impl AuthCache {
|
||||
}
|
||||
}
|
||||
_ if token.starts_with("jwt_") => {
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
if !jwt_secret.is_empty() {
|
||||
let jwt_token = token.trim_start_matches("jwt_");
|
||||
let jwt_token = token.trim_start_matches("jwt_");
|
||||
|
||||
let jwt_result = jsonwebtoken::decode::<JWTAuthClaims>(
|
||||
jwt_token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
|
||||
);
|
||||
let jwt_result = jwt::decode_with_internal_secret::<JWTAuthClaims>(jwt_token).await;
|
||||
|
||||
match jwt_result {
|
||||
Ok(payload) => {
|
||||
if w_id.is_some_and(|w_id| w_id != payload.claims.workspace_id) {
|
||||
tracing::error!("JWT auth error: workspace_id mismatch");
|
||||
return None;
|
||||
}
|
||||
|
||||
let username_override =
|
||||
username_override_from_label(payload.claims.label);
|
||||
let authed = crate::db::ApiAuthed {
|
||||
email: payload.claims.email,
|
||||
username: payload.claims.username,
|
||||
is_admin: payload.claims.is_admin,
|
||||
is_operator: payload.claims.is_operator,
|
||||
groups: payload.claims.groups,
|
||||
folders: payload.claims.folders,
|
||||
scopes: None,
|
||||
username_override,
|
||||
};
|
||||
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
expiry: chrono::Utc
|
||||
.timestamp_nanos(payload.claims.exp as i64 * 1_000_000_000),
|
||||
},
|
||||
);
|
||||
|
||||
Some(authed)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("JWT auth error: {:?}", err);
|
||||
None
|
||||
match jwt_result {
|
||||
Ok(claims) => {
|
||||
if w_id.is_some_and(|w_id| w_id != claims.workspace_id) {
|
||||
tracing::error!("JWT auth error: workspace_id mismatch");
|
||||
return None;
|
||||
}
|
||||
|
||||
let username_override = username_override_from_label(claims.label);
|
||||
let authed = crate::db::ApiAuthed {
|
||||
email: claims.email,
|
||||
username: claims.username,
|
||||
is_admin: claims.is_admin,
|
||||
is_operator: claims.is_operator,
|
||||
groups: claims.groups,
|
||||
folders: claims.folders,
|
||||
scopes: None,
|
||||
username_override,
|
||||
};
|
||||
|
||||
self.cache.insert(
|
||||
key,
|
||||
ExpiringAuthCache {
|
||||
authed: authed.clone(),
|
||||
expiry: chrono::Utc
|
||||
.timestamp_nanos(claims.exp as i64 * 1_000_000_000),
|
||||
},
|
||||
);
|
||||
|
||||
Some(authed)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("JWT auth error: {:?}", err);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
tracing::error!("JWT auth error: no jwt secret set");
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -52,6 +52,7 @@ use crate::{
|
||||
args::WebhookArgs,
|
||||
db::{ApiAuthed, DB},
|
||||
users::fetch_api_authed,
|
||||
utils::RunnableKind,
|
||||
};
|
||||
|
||||
const KEEP_LAST: i64 = 20;
|
||||
@@ -139,7 +140,7 @@ pub struct KafkaTriggerConfig {
|
||||
pub struct SqsTriggerConfig {
|
||||
pub queue_url: String,
|
||||
pub aws_resource_path: String,
|
||||
pub message_attributes: Option<Vec<String>>
|
||||
pub message_attributes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "nats"))]
|
||||
@@ -300,8 +301,7 @@ async fn set_config(
|
||||
#[cfg(feature = "postgres_trigger")]
|
||||
let nc = if let TriggerKind::Postgres = nc.trigger_kind {
|
||||
set_postgres_trigger_config(&w_id, authed.clone(), &db, user_db.clone(), nc).await?
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
nc
|
||||
};
|
||||
|
||||
@@ -362,13 +362,6 @@ struct Capture {
|
||||
trigger_extra: Option<SqlxJson<Box<serde_json::value::RawValue>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListCapturesQuery {
|
||||
trigger_kind: Option<TriggerKind>,
|
||||
|
||||
@@ -32,6 +32,28 @@ async fn current_database(conn: &mut PgConnection) -> Result<String, MigrateErro
|
||||
.await?)
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref OVERRIDDEN_MIGRATIONS: std::collections::HashMap<i64, String> = vec![(20221207103910, include_str!(
|
||||
"../../custom_migrations/create_workspace_without_md5.sql"
|
||||
).to_string()),
|
||||
(20240216100535, include_str!(
|
||||
"../../migrations/20240216100535_improve_policies.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20240403083110, include_str!(
|
||||
"../../migrations/20240403083110_remove_team_id_constraint.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20240613150524, include_str!(
|
||||
"../../migrations/20240613150524_add_job_perms.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20250102145420, include_str!(
|
||||
"../../migrations/20250102145420_more_captures.up.sql"
|
||||
).replace("public.", "")),
|
||||
(20241006144414, include_str!(
|
||||
"../../custom_migrations/grant_all_current_schema.sql"
|
||||
).to_string()),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
struct CustomMigrator {
|
||||
inner: PoolConnection<Postgres>,
|
||||
}
|
||||
@@ -132,12 +154,13 @@ impl Migrate for CustomMigrator {
|
||||
migration.version,
|
||||
migration.description
|
||||
);
|
||||
if migration.version == 20221207103910 {
|
||||
tracing::info!("Skipping migration 20221207103910 to avoid using md5");
|
||||
|
||||
|
||||
if let Some(migration_sql) = OVERRIDDEN_MIGRATIONS.get(&migration.version) {
|
||||
tracing::info!("Using custom migration for version {}", migration.version);
|
||||
|
||||
self.inner
|
||||
.execute(include_str!(
|
||||
"../../custom_migrations/create_workspace_without_md5.sql"
|
||||
))
|
||||
.execute(&**migration_sql)
|
||||
.await?;
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
@@ -528,6 +551,7 @@ async fn v2_finalize(db: &DB) -> Result<(), Error> {
|
||||
)
|
||||
.await?;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -745,6 +769,12 @@ async fn fix_job_completed_index(db: &DB) -> Result<(), Error> {
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
|
||||
run_windmill_migration!("audit_timestamps", db, |tx| {
|
||||
sqlx::query!("CREATE INDEX CONCURRENTLY ix_audit_timestamps ON audit (timestamp DESC)")
|
||||
.execute(db)
|
||||
.await?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::db::ApiAuthed;
|
||||
use crate::triggers::{
|
||||
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
|
||||
};
|
||||
use crate::utils::WithStarredInfoQuery;
|
||||
use crate::utils::{RunnableKind, WithStarredInfoQuery};
|
||||
use crate::{
|
||||
db::DB,
|
||||
schedule::clear_schedule,
|
||||
@@ -65,6 +65,10 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/list_paths", get(list_paths))
|
||||
.route("/history/p/*path", get(get_flow_history))
|
||||
.route("/get_latest_version/*path", get(get_latest_version))
|
||||
.route(
|
||||
"/list_paths_from_workspace_runnable/:runnable_kind/*path",
|
||||
get(list_paths_from_workspace_runnable),
|
||||
)
|
||||
.route(
|
||||
"/history_update/v/:version/p/*path",
|
||||
post(update_flow_history),
|
||||
@@ -324,6 +328,28 @@ async fn check_path_conflict<'c>(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
async fn list_paths_from_workspace_runnable(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, runnable_kind, path)): Path<(String, RunnableKind, StripPath)>,
|
||||
) -> JsonResult<Vec<String>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let runnables = sqlx::query_scalar!(
|
||||
r#"SELECT f.path
|
||||
FROM flow_workspace_runnables fwr
|
||||
JOIN flow f
|
||||
ON fwr.flow_path = f.path AND fwr.workspace_id = f.workspace_id
|
||||
WHERE fwr.runnable_path = $1 AND fwr.runnable_is_flow = $2 AND fwr.workspace_id = $3"#,
|
||||
path.to_path(),
|
||||
matches!(runnable_kind, RunnableKind::Flow),
|
||||
w_id
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(runnables))
|
||||
}
|
||||
|
||||
async fn create_flow(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
|
||||
@@ -116,7 +116,7 @@ mod users;
|
||||
mod users_ee;
|
||||
mod utils;
|
||||
mod variables;
|
||||
mod webhook_util;
|
||||
pub mod webhook_util;
|
||||
#[cfg(feature = "websocket")]
|
||||
mod websocket_triggers;
|
||||
mod workers;
|
||||
|
||||
@@ -18,7 +18,6 @@ use crate::{
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
HTTP_CLIENT,
|
||||
};
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use axum::extract::Multipart;
|
||||
|
||||
use axum::{
|
||||
@@ -42,7 +41,6 @@ use std::{
|
||||
use windmill_audit::audit_ee::audit_log;
|
||||
use windmill_audit::ActionKind;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::error::to_anyhow;
|
||||
|
||||
use windmill_common::{
|
||||
@@ -358,12 +356,6 @@ fn hash_script(ns: &NewScript) -> i64 {
|
||||
dh.finish() as i64
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
async fn create_snapshot_script() -> Result<(StatusCode, String)> {
|
||||
Err(Error::BadRequest("Upgrade to EE to use bundle".to_string()))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
async fn create_snapshot_script(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -404,12 +396,35 @@ async fn create_snapshot_script(
|
||||
})?;
|
||||
|
||||
uploaded = true;
|
||||
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
|
||||
|
||||
if &windmill_common::utils::MODE_AND_ADDONS.mode
|
||||
== &windmill_common::utils::Mode::Standalone
|
||||
{
|
||||
std::fs::create_dir_all(format!(
|
||||
"{}/script_bundle/{}",
|
||||
windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
|
||||
w_id
|
||||
))?;
|
||||
windmill_common::worker::write_file(
|
||||
windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
|
||||
&path,
|
||||
&String::from_utf8_lossy(&data),
|
||||
)?;
|
||||
return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap())));
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
{
|
||||
return Err(Error::ExecutionErr("codebase is an EE feature".to_string()));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
|
||||
if let Err(e) = os
|
||||
.put(&object_store::path::Path::from(path.clone()), data.into())
|
||||
.await
|
||||
|
||||
@@ -2478,6 +2478,22 @@ async fn update_username_in_workpsace<'c>(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE flow_workspace_runnables SET flow_path = REGEXP_REPLACE(flow_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1') WHERE flow_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
old_username,
|
||||
w_id
|
||||
).execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE flow_workspace_runnables SET runnable_path = REGEXP_REPLACE(runnable_path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1') WHERE runnable_path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
old_username,
|
||||
w_id
|
||||
).execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"UPDATE flow_node SET path = REGEXP_REPLACE(path,'u/' || $2 || '/(.*)','u/' || $1 || '/\1') WHERE path LIKE ('u/' || $2 || '/%') AND workspace_id = $3"#,
|
||||
new_username,
|
||||
|
||||
@@ -29,6 +29,13 @@ pub struct WithStarredInfoQuery {
|
||||
pub with_starred_info: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RunnableKind {
|
||||
Script,
|
||||
Flow,
|
||||
}
|
||||
|
||||
pub async fn require_super_admin(db: &DB, email: &str) -> error::Result<()> {
|
||||
let is_admin = is_super_admin_email(db, email).await?;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ lazy_static::lazy_static! {
|
||||
|
||||
pub static ref INSTANCE_EVENTS_WEBHOOK: Option<String> = std::env::var("INSTANCE_EVENTS_WEBHOOK").ok();
|
||||
|
||||
pub static ref WEBHOOK_CACHE: Cache<String, Option<String>> = Cache::new(100);
|
||||
|
||||
}
|
||||
|
||||
pub enum WebhookPayload {
|
||||
@@ -76,7 +78,6 @@ impl WebhookShared {
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap();
|
||||
let cache = Cache::new(100);
|
||||
|
||||
loop {
|
||||
select! {
|
||||
@@ -84,12 +85,12 @@ impl WebhookShared {
|
||||
_ = shutdown_rx.recv() => break,
|
||||
r = rx.recv() => match r {
|
||||
Some(WebhookPayload::WorkspaceEvent(workspace_id, message)) => {
|
||||
let webhook_opt = match cache.get(&workspace_id) {
|
||||
let webhook_opt = match WEBHOOK_CACHE.get(&workspace_id) {
|
||||
Some(guard) => {
|
||||
guard
|
||||
},
|
||||
None => {
|
||||
let Ok(webook_opt) =
|
||||
let Ok(mut webhook_opt) =
|
||||
sqlx::query_scalar!(
|
||||
"SELECT webhook FROM workspace_settings WHERE workspace_id = $1",
|
||||
workspace_id
|
||||
@@ -101,13 +102,17 @@ impl WebhookShared {
|
||||
tracing::error!("Webhook Message to send - but cannot get workspace settings! Workspace: {workspace_id}");
|
||||
continue;
|
||||
};
|
||||
cache.insert(workspace_id, webook_opt.clone());
|
||||
webook_opt
|
||||
if webhook_opt.as_ref().is_some_and(|x| x.is_empty()) {
|
||||
webhook_opt = None;
|
||||
}
|
||||
WEBHOOK_CACHE.insert(workspace_id, webhook_opt.clone());
|
||||
webhook_opt
|
||||
}
|
||||
};
|
||||
if let Some(url) = webhook_opt {
|
||||
#[cfg(feature = "prometheus")]
|
||||
let timer = if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
|
||||
tracing::info!("Sending webhook message to {}", url);
|
||||
let _ = client.post(url).json(&message).send().await;
|
||||
#[cfg(feature = "prometheus")]
|
||||
timer.map(|x| x.stop_and_record());
|
||||
|
||||
@@ -1447,7 +1447,12 @@ async fn user_workspaces(
|
||||
Ok(Json(WorkspaceList { email, workspaces }))
|
||||
}
|
||||
|
||||
async fn check_name_conflict<'c>(tx: &mut Transaction<'c, Postgres>, w_id: &str) -> Result<()> {
|
||||
pub async fn check_w_id_conflict<'c>(tx: &mut Transaction<'c, Postgres>, w_id: &str) -> Result<()> {
|
||||
if w_id == "global" {
|
||||
return Err(windmill_common::error::Error::BadRequest(
|
||||
"'global' is not allowed as a workspace ID".to_string(),
|
||||
));
|
||||
}
|
||||
let exists = sqlx::query_scalar!("SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)", w_id)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?
|
||||
@@ -1505,7 +1510,7 @@ async fn create_workspace(
|
||||
|
||||
let mut tx: Transaction<'_, Postgres> = db.begin().await?;
|
||||
|
||||
check_name_conflict(&mut tx, &nw.id).await?;
|
||||
check_w_id_conflict(&mut tx, &nw.id).await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace
|
||||
(id, name, owner)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
use crate::workspaces::CREATE_WORKSPACE_REQUIRE_SUPERADMIN;
|
||||
use crate::workspaces::{check_w_id_conflict, CREATE_WORKSPACE_REQUIRE_SUPERADMIN};
|
||||
use crate::{db::DB, utils::require_super_admin};
|
||||
|
||||
use axum::{
|
||||
@@ -46,20 +46,7 @@ pub(crate) async fn change_workspace_id(
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let workspace_conflict = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM workspace WHERE id = $1)",
|
||||
&rw.new_id,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if workspace_conflict {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"workspace id {} already used",
|
||||
&rw.new_id
|
||||
)));
|
||||
}
|
||||
check_w_id_conflict(&mut tx, &rw.new_id).await?;
|
||||
|
||||
// duplicate workspace with new id name
|
||||
sqlx::query!(
|
||||
@@ -202,6 +189,14 @@ pub(crate) async fn change_workspace_id(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE flow_workspace_runnables SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
&rw.new_id,
|
||||
&old_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE flow_node SET workspace_id = $1 WHERE workspace_id = $2",
|
||||
&rw.new_id,
|
||||
|
||||
@@ -34,4 +34,5 @@ pub struct ListAuditLogQuery {
|
||||
pub resource: Option<String>,
|
||||
pub before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub all_workspaces: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ edition.workspace = true
|
||||
default = []
|
||||
enterprise = []
|
||||
jemalloc = ["dep:tikv-jemalloc-ctl"]
|
||||
tantivy = []
|
||||
prometheus = ["dep:prometheus"]
|
||||
loki = ["dep:tracing-loki"]
|
||||
benchmark = []
|
||||
@@ -61,6 +62,8 @@ async-stream.workspace = true
|
||||
const_format.workspace = true
|
||||
crc.workspace = true
|
||||
windmill-macros.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
backon.workspace = true
|
||||
|
||||
semver.workspace = true
|
||||
croner = "2.0.6"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
db::Authed,
|
||||
@@ -9,10 +7,6 @@ use crate::{
|
||||
DB,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref JWT_SECRET : Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct JWTAuthClaims {
|
||||
pub email: String,
|
||||
|
||||
@@ -59,6 +59,10 @@ impl Authable for Authed {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref PG_SCHEMA: Option<String> = std::env::var("PG_SCHEMA").ok();
|
||||
}
|
||||
|
||||
impl UserDB {
|
||||
pub fn new(db: DB) -> Self {
|
||||
Self { db }
|
||||
@@ -95,6 +99,12 @@ impl UserDB {
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(schema) = PG_SCHEMA.as_ref() {
|
||||
sqlx::query(&format!("SET LOCAL search_path TO {}", schema))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"SELECT set_config('session.user', $1, true)",
|
||||
authed.username()
|
||||
|
||||
@@ -40,7 +40,7 @@ pub const JWT_SECRET_SETTING: &str = "jwt_secret";
|
||||
pub const EMAIL_DOMAIN_SETTING: &str = "email_domain";
|
||||
pub const OTEL_SETTING: &str = "otel";
|
||||
|
||||
pub const ENV_SETTINGS: [&str; 56] = [
|
||||
pub const ENV_SETTINGS: [&str; 57] = [
|
||||
"DISABLE_NSJAIL",
|
||||
"MODE",
|
||||
"NUM_WORKERS",
|
||||
@@ -97,6 +97,7 @@ pub const ENV_SETTINGS: [&str; 56] = [
|
||||
"OTEL_TRACING",
|
||||
"OTEL_LOGS",
|
||||
"DISABLE_S3_STORE",
|
||||
"PG_SCHEMA",
|
||||
];
|
||||
|
||||
use crate::error;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use crate::error::{self, to_anyhow, Error};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref JWT_SECRET: Arc<RwLock<String>> = Arc::new(RwLock::new("".to_string()));
|
||||
}
|
||||
|
||||
pub async fn encode_with_internal_secret<T: Serialize>(claims: T) -> error::Result<String> {
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
|
||||
if jwt_secret.is_empty() {
|
||||
return Err(Error::internal_err("JWT secret is not set".to_string()));
|
||||
}
|
||||
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
|
||||
&claims,
|
||||
&jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
)
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub async fn decode_with_internal_secret<T: DeserializeOwned>(token: &str) -> error::Result<T> {
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
|
||||
if jwt_secret.is_empty() {
|
||||
return Err(Error::internal_err("JWT secret is not set".to_string()));
|
||||
}
|
||||
|
||||
let result = jsonwebtoken::decode::<T>(
|
||||
token,
|
||||
&jsonwebtoken::DecodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
&jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256),
|
||||
)
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
Ok(result.claims)
|
||||
}
|
||||
@@ -33,6 +33,7 @@ pub mod job_metrics;
|
||||
#[cfg(feature = "parquet")]
|
||||
pub mod job_s3_helpers_ee;
|
||||
pub mod jobs;
|
||||
pub mod jwt;
|
||||
pub mod more_serde;
|
||||
pub mod oauth2;
|
||||
pub mod otel_ee;
|
||||
|
||||
@@ -19,6 +19,8 @@ use crate::{
|
||||
|
||||
use crate::worker::HUB_CACHE_DIR;
|
||||
use anyhow::Context;
|
||||
use backon::ConstantBuilder;
|
||||
use backon::{BackoffBuilder, Retryable};
|
||||
use serde::de::Error as _;
|
||||
use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize};
|
||||
|
||||
@@ -415,6 +417,8 @@ pub async fn get_hub_script_by_path(
|
||||
Some(db),
|
||||
)
|
||||
.await?
|
||||
.error_for_status()
|
||||
.map_err(to_anyhow)?
|
||||
.text()
|
||||
.await
|
||||
.map_err(to_anyhow);
|
||||
@@ -440,6 +444,8 @@ pub async fn get_hub_script_by_path(
|
||||
Some(db),
|
||||
)
|
||||
.await?
|
||||
.error_for_status()
|
||||
.map_err(to_anyhow)?
|
||||
.text()
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
@@ -494,49 +500,67 @@ async fn get_full_hub_script_by_path_inner(
|
||||
) -> crate::error::Result<HubScript> {
|
||||
let hub_base_url = HUB_BASE_URL.read().await.clone();
|
||||
|
||||
let result = http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", hub_base_url, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.json::<HubScript>()
|
||||
.await
|
||||
.context("Decoding hub response to script");
|
||||
let response = (|| async {
|
||||
let response = http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", hub_base_url, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await
|
||||
.and_then(|r| r.error_for_status().map_err(|e| to_anyhow(e).into()));
|
||||
|
||||
match result {
|
||||
Ok(result) => Ok(result),
|
||||
Err(e) => {
|
||||
if hub_base_url != DEFAULT_HUB_BASE_URL
|
||||
&& path
|
||||
.split("/")
|
||||
.next()
|
||||
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < 10_000_000))
|
||||
{
|
||||
tracing::info!(
|
||||
"Not found on private hub, fallback to default hub for {}",
|
||||
path
|
||||
);
|
||||
let value = http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", DEFAULT_HUB_BASE_URL, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.json::<HubScript>()
|
||||
.await
|
||||
.context("Decoding hub response to script")?;
|
||||
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(e)?
|
||||
match response {
|
||||
Ok(response) => Ok(response),
|
||||
Err(e) => {
|
||||
if hub_base_url != DEFAULT_HUB_BASE_URL
|
||||
&& path
|
||||
.split("/")
|
||||
.next()
|
||||
.is_some_and(|x| x.parse::<i32>().is_ok_and(|x| x < 10_000_000))
|
||||
{
|
||||
// TODO: should only fallback to default hub if status is 404 (hub returns 500 currently)
|
||||
tracing::info!(
|
||||
"Not found on private hub, fallback to default hub for {}",
|
||||
path
|
||||
);
|
||||
http_get_from_hub(
|
||||
http_client,
|
||||
&format!("{}/raw2/{}", DEFAULT_HUB_BASE_URL, path),
|
||||
true,
|
||||
None,
|
||||
db,
|
||||
)
|
||||
.await?
|
||||
.error_for_status()
|
||||
.map_err(|e| to_anyhow(e).into())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.retry(
|
||||
ConstantBuilder::default()
|
||||
.with_delay(std::time::Duration::from_secs(5))
|
||||
.with_max_times(2)
|
||||
.build(),
|
||||
)
|
||||
.notify(|err, dur| {
|
||||
tracing::warn!(
|
||||
"Could not get hub script at path {path}, retrying in {dur:#?}, err: {err:#?}"
|
||||
);
|
||||
})
|
||||
.sleep(tokio::time::sleep)
|
||||
.await?;
|
||||
|
||||
let script = response
|
||||
.json::<HubScript>()
|
||||
.await
|
||||
.context(format!("Decoding hub response for script at path {path}"))?;
|
||||
|
||||
Ok(script)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
|
||||
@@ -53,6 +53,76 @@ lazy_static::lazy_static! {
|
||||
GIT_VERSION
|
||||
}
|
||||
).unwrap_or(Version::new(0, 1, 0));
|
||||
|
||||
|
||||
pub static ref MODE_AND_ADDONS: ModeAndAddons = {
|
||||
let mut search_addon = false;
|
||||
let mode = std::env::var("MODE")
|
||||
.map(|x| x.to_lowercase())
|
||||
.map(|x| {
|
||||
if &x == "server" {
|
||||
println!("Binary is in 'server' mode");
|
||||
Mode::Server
|
||||
} else if &x == "worker" {
|
||||
tracing::info!("Binary is in 'worker' mode");
|
||||
#[cfg(windows)]
|
||||
{
|
||||
println!("It is highly recommended to use the agent mode instead on windows (MODE=agent) and to pass a BASE_INTERNAL_URL");
|
||||
}
|
||||
Mode::Worker
|
||||
} else if &x == "agent" {
|
||||
println!("Binary is in 'agent' mode");
|
||||
if std::env::var("BASE_INTERNAL_URL").is_err() {
|
||||
panic!("BASE_INTERNAL_URL is required in agent mode")
|
||||
}
|
||||
if std::env::var("JOB_TOKEN").is_err() {
|
||||
println!("JOB_TOKEN is not passed, hence workers will still need to create permissions for each job and the DATABASE_URL needs to be of a role that can INSERT into the job_perms table")
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
panic!("Agent mode is only available in the EE, ignoring...");
|
||||
}
|
||||
#[cfg(feature = "enterprise")]
|
||||
Mode::Agent
|
||||
} else if &x == "indexer" {
|
||||
tracing::info!("Binary is in 'indexer' mode");
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
{
|
||||
eprintln!("Cannot start the indexer because tantivy is not included in this binary/image. Make sure you are using the EE image if you want to access the full text search features.");
|
||||
panic!("Indexer mode requires compiling with the tantivy feature flag.");
|
||||
}
|
||||
#[cfg(feature = "tantivy")]
|
||||
Mode::Indexer
|
||||
} else if &x == "standalone+search"{
|
||||
search_addon = true;
|
||||
println!("Binary is in 'standalone' mode with search enabled");
|
||||
Mode::Standalone
|
||||
}
|
||||
else {
|
||||
if &x != "standalone" {
|
||||
eprintln!("mode not recognized, defaulting to standalone: {x}");
|
||||
} else {
|
||||
println!("Binary is in 'standalone' mode");
|
||||
}
|
||||
Mode::Standalone
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
tracing::info!("Mode not specified, defaulting to standalone");
|
||||
Mode::Standalone
|
||||
});
|
||||
ModeAndAddons {
|
||||
indexer: search_addon,
|
||||
mode,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ModeAndAddons {
|
||||
pub indexer: bool,
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
|
||||
@@ -104,6 +104,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref DISABLE_FLOW_SCRIPT: bool = std::env::var("DISABLE_FLOW_SCRIPT").ok().is_some_and(|x| x == "1" || x == "true");
|
||||
}
|
||||
|
||||
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
|
||||
pub static MIN_VERSION_IS_LATEST: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
fn format_pull_query(peek: String) -> String {
|
||||
|
||||
@@ -247,7 +247,7 @@ pub async fn cancel_job<'c>(
|
||||
while !jobs.is_empty() {
|
||||
let p_job = jobs.pop();
|
||||
let new_jobs = sqlx::query_scalar!(
|
||||
"SELECT id AS \"id!\" FROM v2_job WHERE parent_job = $1 AND workspace_id = $2",
|
||||
"SELECT id AS \"id!\" FROM v2_job_queue INNER JOIN v2_job USING (id) WHERE parent_job = $1 AND v2_job.workspace_id = $2",
|
||||
p_job,
|
||||
w_id
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ use windmill_common::{
|
||||
get_latest_hash_for_path,
|
||||
jobs::QueuedJob,
|
||||
scripts::ScriptLang,
|
||||
worker::{exists_in_cache, save_cache, write_file},
|
||||
worker::{exists_in_cache, save_cache, write_file, DISABLE_BUNDLING},
|
||||
DB,
|
||||
};
|
||||
|
||||
@@ -579,20 +579,22 @@ pub async fn generate_bun_bundle(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<()> {
|
||||
use crate::global_cache::extract_tar;
|
||||
|
||||
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
|
||||
let bun_cache_path = format!("{}/{}", crate::ROOT_CACHE_NOMOUNT_DIR, path);
|
||||
let bun_cache_path = format!(
|
||||
"{}/{}",
|
||||
windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
|
||||
path
|
||||
);
|
||||
let is_tar = id.ends_with(".tar");
|
||||
|
||||
let dst = format!(
|
||||
"{job_dir}/{}",
|
||||
if is_tar { "codebase.tar" } else { "main.js" }
|
||||
);
|
||||
let dirs_splitted = bun_cache_path.split("/").collect_vec();
|
||||
tokio::fs::create_dir_all(dirs_splitted[..dirs_splitted.len() - 1].join("/")).await?;
|
||||
|
||||
if tokio::fs::metadata(&bun_cache_path).await.is_ok() {
|
||||
tracing::info!("loading {bun_cache_path} from cache");
|
||||
if is_tar {
|
||||
@@ -604,35 +606,47 @@ pub async fn pull_codebase(w_id: &str, id: &str, job_dir: &str) -> Result<()> {
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_dir(&bun_cache_path, &dst)?;
|
||||
}
|
||||
} else if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
let bytes = attempt_fetch_bytes(os, &path).await?;
|
||||
|
||||
tokio::fs::write(&bun_cache_path, &bytes).await?;
|
||||
if is_tar {
|
||||
extract_tar(bytes, job_dir).await?;
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
tokio::fs::symlink(bun_cache_path, dst).await?;
|
||||
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_dir(&bun_cache_path, &dst)?;
|
||||
} else {
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
{
|
||||
if &windmill_common::utils::MODE_AND_ADDONS.mode
|
||||
== &windmill_common::utils::Mode::Standalone
|
||||
{
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"(standalone bundle test mode) could not find codebase at {bun_cache_path}"
|
||||
)));
|
||||
} else {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"codebase is an EE feature".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// extract_tar(bytes, job_dir).await?;
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
{
|
||||
let dirs_splitted = bun_cache_path.split("/").collect_vec();
|
||||
tokio::fs::create_dir_all(dirs_splitted[..dirs_splitted.len() - 1].join("/")).await?;
|
||||
|
||||
let bytes = attempt_fetch_bytes(os, &path).await?;
|
||||
|
||||
tokio::fs::write(&bun_cache_path, &bytes).await?;
|
||||
if is_tar {
|
||||
extract_tar(bytes, job_dir).await?;
|
||||
} else {
|
||||
#[cfg(unix)]
|
||||
tokio::fs::symlink(bun_cache_path, dst).await?;
|
||||
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_dir(&bun_cache_path, &dst)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
|
||||
pub async fn pull_codebase(_w_id: &str, _id: &str, _job_dir: &str) -> Result<()> {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"codebase is an EE feature".to_string(),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn prebundle_bun_script(
|
||||
@@ -660,7 +674,7 @@ pub async fn prebundle_bun_script(
|
||||
return Ok(());
|
||||
}
|
||||
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
|
||||
if annotation.nobundling {
|
||||
if annotation.nobundling || *DISABLE_BUNDLING {
|
||||
return Ok(());
|
||||
}
|
||||
let origin = format!("{job_dir}/main.js");
|
||||
@@ -802,23 +816,25 @@ pub async fn handle_bun_job(
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
|
||||
|
||||
let (mut has_bundle_cache, cache_logs, local_path, remote_path) =
|
||||
if requirements_o.is_some() && !annotation.nobundling && codebase.is_none() {
|
||||
let (local_path, remote_path) = compute_bundle_local_and_remote_path(
|
||||
inner_content,
|
||||
requirements_o,
|
||||
job.script_path(),
|
||||
Some(db.clone()),
|
||||
&job.workspace_id,
|
||||
)
|
||||
.await;
|
||||
let (mut has_bundle_cache, cache_logs, local_path, remote_path) = if requirements_o.is_some()
|
||||
&& !annotation.nobundling
|
||||
&& !*DISABLE_BUNDLING
|
||||
&& codebase.is_none()
|
||||
{
|
||||
let (local_path, remote_path) = compute_bundle_local_and_remote_path(
|
||||
inner_content,
|
||||
requirements_o,
|
||||
job.script_path(),
|
||||
Some(db.clone()),
|
||||
&job.workspace_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (cache, logs) =
|
||||
windmill_common::worker::load_cache(&local_path, &remote_path).await;
|
||||
(cache, logs, local_path, remote_path)
|
||||
} else {
|
||||
(false, "".to_string(), "".to_string(), "".to_string())
|
||||
};
|
||||
let (cache, logs) = windmill_common::worker::load_cache(&local_path, &remote_path).await;
|
||||
(cache, logs, local_path, remote_path)
|
||||
} else {
|
||||
(false, "".to_string(), "".to_string(), "".to_string())
|
||||
};
|
||||
|
||||
if !codebase.is_some() && !has_bundle_cache {
|
||||
let _ = write_file(job_dir, "main.ts", inner_content)?;
|
||||
@@ -835,13 +851,6 @@ pub async fn handle_bun_job(
|
||||
let main_override = job.script_entrypoint_override.as_deref();
|
||||
let apply_preprocessor = !job.is_flow_step && job.preprocessed == Some(false);
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if annotation.nodejs || annotation.npm {
|
||||
return Err(error::Error::ExecutionErr(
|
||||
"Nodejs / npm mode is an EE feature".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if has_bundle_cache {
|
||||
let target;
|
||||
let symlink;
|
||||
@@ -1088,6 +1097,7 @@ try {{
|
||||
|
||||
let build_cache = !has_bundle_cache
|
||||
&& !annotation.nobundling
|
||||
&& !*DISABLE_BUNDLING
|
||||
&& !codebase.is_some()
|
||||
&& (requirements_o.is_some() || annotation.native);
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
// #[cfg(feature = "enterprise")]
|
||||
// use rand::Rng;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use tokio::time::Instant;
|
||||
use windmill_common::error;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use object_store::ObjectStore;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_common::error;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -109,7 +106,6 @@ pub async fn pull_from_tar(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
pub async fn extract_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
|
||||
use bytes::Buf;
|
||||
use tokio::fs::{self};
|
||||
|
||||
@@ -376,9 +376,10 @@ fn replace_with_await(expr: String, fn_name: &str) -> String {
|
||||
}
|
||||
lazy_static! {
|
||||
static ref RE: Regex =
|
||||
Regex::new(r#"(?m)(?P<r>results(?:(?:\.[a-zA-Z_0-9]+)|(?:\[\".*?\"\])))"#).unwrap();
|
||||
Regex::new(r#"(?m)(?P<r>results(?:\?)?(?:(?:\.[a-zA-Z_0-9]+)|(?:\[\".*?\"\])))"#).unwrap();
|
||||
static ref RE_FULL: Regex =
|
||||
Regex::new(r"(?m)^results\.([a-zA-Z_0-9]+)(?:\[(\d+)\])?((?:\.[a-zA-Z_0-9]+)+)?$").unwrap();
|
||||
Regex::new(r"(?m)^results(?:\?)?\.([a-zA-Z_0-9]+)(?:\[(\d+)\])?((?:\.[a-zA-Z_0-9]+)+)?$")
|
||||
.unwrap();
|
||||
static ref RE_PROXY: Regex =
|
||||
Regex::new(r"^(https?)://(([^:@\s]+):([^:@\s]+)@)?([^:@\s]+)(:(\d+))?$").unwrap();
|
||||
}
|
||||
|
||||
@@ -315,6 +315,11 @@ impl PyVersion {
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -354,6 +359,11 @@ impl PyVersion {
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -595,6 +605,11 @@ pub async fn uv_pip_compile(
|
||||
child_cmd
|
||||
.env("SystemRoot", SYSTEM_ROOT.as_str())
|
||||
.env("USERPROFILE", crate::USERPROFILE_ENV.as_str())
|
||||
.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
|
||||
)
|
||||
.env(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
@@ -1110,6 +1125,11 @@ mount {{
|
||||
{
|
||||
python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str());
|
||||
python_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
|
||||
python_cmd.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
|
||||
);
|
||||
}
|
||||
|
||||
start_child_process(python_cmd, &python_path).await?
|
||||
@@ -1453,15 +1473,6 @@ async fn handle_python_deps(
|
||||
}
|
||||
};
|
||||
|
||||
let requirements_lines: Vec<&str> = if requirements.len() > 0 {
|
||||
requirements
|
||||
.split("\n")
|
||||
.filter(|x| !x.starts_with("--") && !x.trim().is_empty())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
/*
|
||||
For deployed scripts we want to find out version in following order:
|
||||
1. Assigned version (written in lockfile)
|
||||
@@ -1472,20 +1483,9 @@ async fn handle_python_deps(
|
||||
2. Instance version
|
||||
3. Latest Stable
|
||||
*/
|
||||
let requirements_lines = split_requirements(requirements.as_str());
|
||||
let final_version = if is_deployed {
|
||||
// If script is deployed we can try to parse first line to get assigned version
|
||||
if let Some(v) = requirements_lines
|
||||
.get(0)
|
||||
.and_then(|line| PyVersion::parse_version(line))
|
||||
{
|
||||
// We have valid assigned version, we use it
|
||||
v
|
||||
} else {
|
||||
// If there is no assigned version in lockfile we automatically fallback to 3.11
|
||||
// In this case we have dependencies, but no associated python version
|
||||
// This is the case for old deployed scripts
|
||||
PyVersion::Py311
|
||||
}
|
||||
get_pyv_from_requirements_lines(&requirements_lines)
|
||||
} else {
|
||||
// This is not deployed script, meaning we test run it (Preview)
|
||||
annotated_pyv.unwrap_or(instance_pyv)
|
||||
@@ -1683,6 +1683,11 @@ async fn spawn_uv_install(
|
||||
"TMP",
|
||||
std::env::var("TMP").unwrap_or_else(|_| String::from("/tmp")),
|
||||
)
|
||||
.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
.unwrap_or_else(|_| format!("{}\\AppData\\Local", HOME_ENV.as_str())),
|
||||
)
|
||||
.args(&command_args[1..])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
@@ -2249,6 +2254,29 @@ pub async fn handle_python_reqs(
|
||||
};
|
||||
}
|
||||
|
||||
fn split_requirements(requirements: &str) -> Vec<&str> {
|
||||
requirements
|
||||
.split("\n")
|
||||
.filter(|x| !x.trim_start().starts_with("--") && !x.trim().is_empty())
|
||||
.collect()
|
||||
}
|
||||
/// Check requirements/lockfile to figure out python version assigned to it.
|
||||
fn get_pyv_from_requirements_lines(requirements_lines: &[&str]) -> PyVersion {
|
||||
// If script is deployed we can try to parse first line to get assigned version
|
||||
if let Some(v) = requirements_lines
|
||||
.get(0)
|
||||
.and_then(|line| PyVersion::parse_version(*line))
|
||||
{
|
||||
// We have valid assigned version, we use it
|
||||
v
|
||||
} else {
|
||||
// If there is no assigned version in lockfile we automatically fallback to 3.11
|
||||
// In this case we have dependencies, but no associated python version
|
||||
// This is the case for old deployed scripts
|
||||
PyVersion::Py311
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
use crate::JobCompletedSender;
|
||||
#[cfg(feature = "enterprise")]
|
||||
@@ -2419,7 +2447,13 @@ for line in sys.stdin:
|
||||
);
|
||||
proc_envs.insert("BASE_URL".to_string(), base_internal_url.to_string());
|
||||
|
||||
let py_version = PyVersion::from_instance_version().await;
|
||||
let py_version = if let Some(requirements) = requirements_o {
|
||||
get_pyv_from_requirements_lines(&split_requirements(requirements.as_str()))
|
||||
} else {
|
||||
tracing::warn!(workspace_id = %w_id, "lockfile is empty for dedicated worker, thus python version cannot be inferred. Fallback to 3.11");
|
||||
PyVersion::Py311
|
||||
};
|
||||
|
||||
let python_path = get_python_path(
|
||||
py_version,
|
||||
worker_name,
|
||||
|
||||
@@ -653,7 +653,6 @@ pub async fn handle_job_error(
|
||||
if let Some(f) = update_job_future {
|
||||
let _ = f().await;
|
||||
}
|
||||
tracing::error!(job_id = %job.id, "error handling job: {err:?} {} {} {}", job.id, job.workspace_id, job.created_by);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
|
||||
use windmill_common::{
|
||||
apps::AppScriptId,
|
||||
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms, JWT_SECRET},
|
||||
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms},
|
||||
cache::{ScriptData, ScriptMetadata},
|
||||
jwt,
|
||||
scripts::PREVIEW_IS_TAR_CODEBASE_HASH,
|
||||
utils::WarnAfterExt,
|
||||
worker::{
|
||||
get_memory, get_vcpus, get_windmill_memory_usage, get_worker_memory_usage, write_file,
|
||||
ROOT_CACHE_DIR, TMP_DIR,
|
||||
ROOT_CACHE_DIR, ROOT_CACHE_NOMOUNT_DIR, TMP_DIR,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -205,12 +206,6 @@ pub async fn create_token_for_owner(
|
||||
return Ok(token.clone());
|
||||
}
|
||||
|
||||
let jwt_secret = JWT_SECRET.read().await;
|
||||
|
||||
if jwt_secret.is_empty() {
|
||||
return Err(Error::internal_err("No JWT secret found".to_string()));
|
||||
}
|
||||
|
||||
let job_authed = match sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT * FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
@@ -248,22 +243,13 @@ pub async fn create_token_for_owner(
|
||||
scopes: None,
|
||||
};
|
||||
|
||||
let token = jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
|
||||
&payload,
|
||||
&jsonwebtoken::EncodingKey::from_secret(jwt_secret.as_bytes()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
Error::internal_err(format!(
|
||||
"Could not encode JWT token for job {job_id}: {:?}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
let token = jwt::encode_with_internal_secret(&payload)
|
||||
.await
|
||||
.with_context(|| format!("Could not encode JWT token for job {job_id}"))?;
|
||||
|
||||
Ok(format!("jwt_{}", token))
|
||||
}
|
||||
|
||||
pub const ROOT_CACHE_NOMOUNT_DIR: &str = concatcp!(TMP_DIR, "/cache_nomount/");
|
||||
|
||||
pub const PY310_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_310");
|
||||
pub const PY311_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_311");
|
||||
pub const PY312_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "python_312");
|
||||
@@ -286,6 +272,7 @@ pub const RUST_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "rust");
|
||||
pub const CSHARP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "csharp");
|
||||
pub const BUN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "bun");
|
||||
pub const BUN_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "bun");
|
||||
pub const BUN_CODEBASE_BUNDLE_CACHE_DIR: &str = concatcp!(ROOT_CACHE_NOMOUNT_DIR, "script_bundle");
|
||||
|
||||
pub const GO_BIN_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "gobin");
|
||||
pub const POWERSHELL_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "powershell");
|
||||
|
||||
@@ -620,6 +620,13 @@ pub async fn handle_flow_dependency_job(
|
||||
|
||||
tx = clear_dependency_parent_path(&parent_path, &job_path, &job.workspace_id, "flow", tx)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"DELETE FROM flow_workspace_runnables WHERE flow_path = $1 AND workspace_id = $2",
|
||||
job_path,
|
||||
job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
let modified_ids;
|
||||
let errors;
|
||||
(flow.modules, tx, modified_ids, errors) = lock_modules(
|
||||
@@ -981,6 +988,27 @@ async fn lock_modules<'c>(
|
||||
}
|
||||
.into();
|
||||
}
|
||||
FlowModuleValue::Script { path, hash, .. } if !path.starts_with("hub/") => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow_workspace_runnables (flow_path, runnable_path, script_hash, runnable_is_flow, workspace_id) VALUES ($1, $2, $3, FALSE, $4) ON CONFLICT DO NOTHING",
|
||||
job_path,
|
||||
path,
|
||||
hash.map(|h| h.0),
|
||||
job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
FlowModuleValue::Flow { path, .. } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow_workspace_runnables (flow_path, runnable_path, runnable_is_flow, workspace_id) VALUES ($1, $2, TRUE, $3) ON CONFLICT DO NOTHING",
|
||||
job_path,
|
||||
path,
|
||||
job.workspace_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
modified_ids.extend(nmodified_ids);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.467.1";
|
||||
export const VERSION = "v1.473.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.467.1";
|
||||
export const VERSION = "1.473.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
+31
-33
@@ -356,12 +356,12 @@ function ZipFSElement(
|
||||
)
|
||||
? "flow"
|
||||
: p.endsWith("app.json")
|
||||
? "app"
|
||||
: p.endsWith("script.json")
|
||||
? "script"
|
||||
: p.endsWith("resource.json")
|
||||
? "resource"
|
||||
: "other";
|
||||
? "app"
|
||||
: p.endsWith("script.json")
|
||||
? "script"
|
||||
: p.endsWith("resource.json")
|
||||
? "resource"
|
||||
: "other";
|
||||
|
||||
const isJson = p.endsWith(".json");
|
||||
|
||||
@@ -391,7 +391,7 @@ function ZipFSElement(
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, s.path),
|
||||
async *getChildren() {},
|
||||
async *getChildren() { },
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return s.content;
|
||||
@@ -402,7 +402,7 @@ function ZipFSElement(
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, "flow.yaml"),
|
||||
async *getChildren() {},
|
||||
async *getChildren() { },
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return yamlStringify(flow, yamlOptions);
|
||||
@@ -418,7 +418,7 @@ function ZipFSElement(
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, s.path),
|
||||
async *getChildren() {},
|
||||
async *getChildren() { },
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return s.content;
|
||||
@@ -429,7 +429,7 @@ function ZipFSElement(
|
||||
yield {
|
||||
isDirectory: false,
|
||||
path: path.join(finalPath, "app.yaml"),
|
||||
async *getChildren() {},
|
||||
async *getChildren() { },
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return yamlStringify(app, yamlOptions);
|
||||
@@ -494,7 +494,7 @@ function ZipFSElement(
|
||||
r.push({
|
||||
isDirectory: false,
|
||||
path: removeSuffix(finalPath, ".json") + ".lock",
|
||||
async *getChildren() {},
|
||||
async *getChildren() { },
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return lock;
|
||||
@@ -517,7 +517,7 @@ function ZipFSElement(
|
||||
removeSuffix(finalPath, ".resource.json") +
|
||||
".resource.file." +
|
||||
formatExtension,
|
||||
async *getChildren() {},
|
||||
async *getChildren() { },
|
||||
// deno-lint-ignore require-await
|
||||
async getContentText() {
|
||||
return fileContent;
|
||||
@@ -578,19 +578,19 @@ export async function* readDirRecursiveWithIgnore(
|
||||
// getContentBytes(): Promise<Uint8Array>;
|
||||
getContentText(): Promise<string>;
|
||||
}[] = [
|
||||
{
|
||||
path: root.path,
|
||||
ignored: ignore(root.path, root.isDirectory),
|
||||
isDirectory: root.isDirectory,
|
||||
c: root.getChildren,
|
||||
// getContentBytes(): Promise<Uint8Array> {
|
||||
// throw undefined;
|
||||
// },
|
||||
getContentText(): Promise<string> {
|
||||
throw undefined;
|
||||
{
|
||||
path: root.path,
|
||||
ignored: ignore(root.path, root.isDirectory),
|
||||
isDirectory: root.isDirectory,
|
||||
c: root.getChildren,
|
||||
// getContentBytes(): Promise<Uint8Array> {
|
||||
// throw undefined;
|
||||
// },
|
||||
getContentText(): Promise<string> {
|
||||
throw undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const e = stack.pop()!;
|
||||
@@ -721,9 +721,9 @@ async function compareDynFSElement(
|
||||
): Promise<Change[]> {
|
||||
const [m1, m2] = els2
|
||||
? await Promise.all([
|
||||
elementsToMap(els1, ignore, json, skips),
|
||||
elementsToMap(els2, ignore, json, skips),
|
||||
])
|
||||
elementsToMap(els1, ignore, json, skips),
|
||||
elementsToMap(els2, ignore, json, skips),
|
||||
])
|
||||
: [await elementsToMap(els1, ignore, json, skips), {}];
|
||||
|
||||
const changes: Change[] = [];
|
||||
@@ -1273,8 +1273,8 @@ function prettyChanges(changes: Change[]) {
|
||||
log.info(
|
||||
colors.yellow(
|
||||
`~ ${getTypeStrFromPath(change.path)} ` +
|
||||
change.path +
|
||||
(change.codebase ? ` (codebase changed)` : "")
|
||||
change.path +
|
||||
(change.codebase ? ` (codebase changed)` : "")
|
||||
)
|
||||
);
|
||||
if (change.before != change.after) {
|
||||
@@ -1479,8 +1479,7 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
}
|
||||
const groupedChangesArray = Array.from(groupedChanges.entries());
|
||||
log.info(
|
||||
`found changes for ${
|
||||
groupedChangesArray.length
|
||||
`found changes for ${groupedChangesArray.length
|
||||
} items with a total of ${groupedChangesArray.reduce(
|
||||
(acc, [_, changes]) => acc + changes.length,
|
||||
0
|
||||
@@ -1787,8 +1786,7 @@ export async function push(opts: GlobalOptions & SyncOptions) {
|
||||
}
|
||||
log.info(
|
||||
colors.bold.green.underline(
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${
|
||||
workspace.workspaceId
|
||||
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId
|
||||
} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
|
||||
)
|
||||
);
|
||||
|
||||
+4
-2
@@ -161,7 +161,7 @@ async function add(
|
||||
undefined,
|
||||
{
|
||||
value,
|
||||
is_secret: !opts.public,
|
||||
is_secret: !opts.public && !opts.plainSecrets,
|
||||
description: "",
|
||||
},
|
||||
opts.plainSecrets ?? false
|
||||
@@ -184,7 +184,9 @@ const command = new Command()
|
||||
"Create a new variable on the remote. This will update the variable if it already exists."
|
||||
)
|
||||
.arguments("<value:string> <remote_path:string>")
|
||||
.option("--public", "Make a public variable")
|
||||
.option("--plain-secrets", "Push secrets as plain text")
|
||||
.option("--public", "Legacy option, use --plain-secrets instead")
|
||||
|
||||
.action(add as any);
|
||||
|
||||
export default command;
|
||||
|
||||
Generated
+10
-10
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.467.1",
|
||||
"version": "1.473.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.467.1",
|
||||
"version": "1.473.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
@@ -70,7 +70,7 @@
|
||||
"windmill-parser-wasm-csharp": "^1.437.1",
|
||||
"windmill-parser-wasm-go": "^1.429.0",
|
||||
"windmill-parser-wasm-php": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.467.1",
|
||||
"windmill-parser-wasm-regex": "^1.439.0",
|
||||
"windmill-parser-wasm-rust": "^1.429.0",
|
||||
"windmill-parser-wasm-ts": "^1.429.0",
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.32.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
|
||||
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz",
|
||||
"integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
@@ -12594,9 +12594,9 @@
|
||||
"integrity": "sha512-SGJAtNpfdRZftkGboxWsm/yQDnJBJodwPQUbX2cWk/aoNook6ULesZwsYtBC9WN1VH6TIskLiVPohMmu6jtXmw=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-py": {
|
||||
"version": "1.429.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.429.0.tgz",
|
||||
"integrity": "sha512-cqc+tblQVHVrc8wNA4esVYD1Dv59XQJ4mHXFFPa8Lx5UjXv7FO/0VQxyQuRyXaP/V6J6DDy0oeN107eFNLxrBg=="
|
||||
"version": "1.467.1",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.467.1.tgz",
|
||||
"integrity": "sha512-Px/UvNjCCScf5V7B90PirxYe7Mn/zMnhk++cLuvlg08j45GR/mzFOIfC7Xk9e555ppX3gTlCk+auH4aIlG2tVw=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-regex": {
|
||||
"version": "1.439.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.467.1",
|
||||
"version": "1.473.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -9,7 +9,7 @@
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .",
|
||||
"package": "./preprocess_pkg_toggle.sh && svelte-package -o package && ./preprocess_pkg_toggle.sh",
|
||||
"package": "svelte-package -o package",
|
||||
"generate-backend-client": "openapi-ts --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions --enums javascript --format false",
|
||||
"generate-backend-client-mac": "openapi-ts --input ../backend/windmill-api/openapi.yaml --output ./src/lib/gen --useOptions --enums javascript",
|
||||
"pretest": "tsc --incremental -p tests/tsconfig.json",
|
||||
@@ -84,7 +84,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@codingame/monaco-vscode-configuration-service-override": "~11.1.2",
|
||||
"@codingame/monaco-vscode-standalone-css-language-features": "~11.1.2",
|
||||
@@ -120,8 +120,8 @@
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~11.1.2",
|
||||
"monaco-editor-wrapper": "6.1.1",
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-graphql": "^1.6.0",
|
||||
"monaco-languageclient": "9.1.1",
|
||||
"monaco-vim": "^0.4.1",
|
||||
"ol": "^7.4.0",
|
||||
"openai": "^4.57.2",
|
||||
@@ -145,7 +145,7 @@
|
||||
"windmill-parser-wasm-csharp": "^1.437.1",
|
||||
"windmill-parser-wasm-go": "^1.429.0",
|
||||
"windmill-parser-wasm-php": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.429.0",
|
||||
"windmill-parser-wasm-py": "^1.467.1",
|
||||
"windmill-parser-wasm-regex": "^1.439.0",
|
||||
"windmill-parser-wasm-rust": "^1.429.0",
|
||||
"windmill-parser-wasm-ts": "^1.429.0",
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define the file to modify
|
||||
FILE="src/lib/components/apps/editor/AppEditorHeader.svelte"
|
||||
|
||||
# Function to toggle commenting of a line
|
||||
toggle_line_comment() {
|
||||
local line="$1"
|
||||
if grep -q "^//$line" "$FILE"; then
|
||||
# Uncomment the line
|
||||
sed -i "s|^//$line|$line|" "$FILE"
|
||||
else
|
||||
# Comment the line
|
||||
sed -i "s|^$line|//$line|" "$FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to toggle commenting of the first matching block
|
||||
toggle_first_block_comment() {
|
||||
local start_marker="$1"
|
||||
local end_marker="$2"
|
||||
|
||||
# Escape the end marker for use in sed
|
||||
local escaped_end_marker=$(echo "$end_marker" | sed 's|/|\\/|g')
|
||||
|
||||
# Check if the block is already commented
|
||||
if sed -n "/$start_marker/,/$escaped_end_marker/p" "$FILE" | grep -q "^<!--"; then
|
||||
# Uncomment the block
|
||||
sed -i "/$start_marker/,/$escaped_end_marker/{
|
||||
s|^<!-- ||
|
||||
s| -->$||
|
||||
}" "$FILE"
|
||||
else
|
||||
# Comment the block
|
||||
sed -i "/$start_marker/,/$escaped_end_marker/{
|
||||
s|^|<!-- |
|
||||
s|$| -->|
|
||||
}" "$FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Define the line and block
|
||||
LINE=" import UnsavedConfirmationModal "
|
||||
BLOCK_START="<UnsavedConfirmationModal"
|
||||
BLOCK_END="/>"
|
||||
|
||||
# Toggle the line comment
|
||||
toggle_line_comment "$LINE"
|
||||
|
||||
# Toggle the first block comment
|
||||
toggle_first_block_comment "$BLOCK_START" "$BLOCK_END"
|
||||
|
||||
echo "Toggled line and first block comments in $FILE."
|
||||
Vendored
+5
-5
@@ -1,7 +1,7 @@
|
||||
declare namespace svelte.JSX {
|
||||
interface DOMAttributes<T> {
|
||||
onclick_outside?: CompositionEventHandler<T>
|
||||
onpointerdown_outside?: (event: CustomEvent) => void
|
||||
onpointerdown_connecting?: (event: CustomEvent) => void
|
||||
declare namespace svelteHTML {
|
||||
interface HTMLAttributes<T> {
|
||||
'on:click_outside'?: (event: CustomEvent) => void
|
||||
'on:pointerdown_outside'?: (event: CustomEvent) => void
|
||||
'on:pointerdown_connecting'?: (event: CustomEvent) => void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { globalEmailInvite, superadmin, workspaceStore } from '$lib/stores'
|
||||
import { SettingService, UserService, WorkspaceService } from '$lib/gen'
|
||||
import { Button, Popup } from './common'
|
||||
import { Button } from './common'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { goto } from '$lib/navigation'
|
||||
@@ -72,64 +73,61 @@
|
||||
let selected: 'operator' | 'developer' | 'admin' = 'developer'
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Popover placement="bottom-end">
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: UserPlus }}>
|
||||
Add new user
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col w-72 p-2">
|
||||
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex flex-col w-72 p-4">
|
||||
<span class="text-sm mb-2 leading-6 font-semibold">Add a new user</span>
|
||||
|
||||
<span class="text-xs mb-1 leading-6">Email</span>
|
||||
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
|
||||
<span class="text-xs mb-1 leading-6">Email</span>
|
||||
<input type="email mb-1" on:keyup={handleKeyUp} placeholder="email" bind:value={email} />
|
||||
|
||||
{#if !automateUsernameCreation}
|
||||
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
|
||||
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
|
||||
{/if}
|
||||
{#if !automateUsernameCreation}
|
||||
<span class="text-xs mb-1 pt-2 leading-6">Username</span>
|
||||
<input type="text" on:keyup={handleKeyUp} placeholder="username" bind:value={username} />
|
||||
{/if}
|
||||
|
||||
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
|
||||
<ToggleButtonGroup bind:selected class="mb-4">
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
|
||||
<ToggleButtonGroup bind:selected class="mb-4" let:item>
|
||||
<ToggleButton
|
||||
value="operator"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="developer"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="admin"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
label="Operator"
|
||||
tooltip="An operator can only execute and view scripts/flows/apps from your workspace, and only those that he has visibility on."
|
||||
/>
|
||||
<ToggleButton
|
||||
position="center"
|
||||
value="developer"
|
||||
size="sm"
|
||||
label="Developer"
|
||||
tooltip="A Developer can execute and view scripts/flows/apps, but they can also create new ones and edit those they are allowed to by their path (either u/ or Writer or Admin of their folder found at /f)."
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value="admin"
|
||||
size="sm"
|
||||
label="Admin"
|
||||
tooltip="An admin has full control over a specific Windmill workspace, including the ability to manage users, edit entities, and control permissions within the workspace."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="blue"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
addUser().then(() => {
|
||||
// @ts-ignore
|
||||
email = undefined
|
||||
// @ts-ignore
|
||||
username = undefined
|
||||
})
|
||||
}}
|
||||
disabled={email === undefined || (!automateUsernameCreation && username === undefined)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</Popup>
|
||||
on:click={() => {
|
||||
addUser().then(() => {
|
||||
// @ts-ignore
|
||||
email = undefined
|
||||
// @ts-ignore
|
||||
username = undefined
|
||||
})
|
||||
}}
|
||||
disabled={email === undefined || (!automateUsernameCreation && username === undefined)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import TestConnection from './TestConnection.svelte'
|
||||
import SupabaseIcon from './icons/SupabaseIcon.svelte'
|
||||
import Popup from './common/popup/Popup.svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
|
||||
@@ -129,13 +129,12 @@
|
||||
/>
|
||||
<TestConnection {resourceType} {args} />
|
||||
{#if resourceType == 'postgresql'}
|
||||
<Popup
|
||||
let:close
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
placement: 'bottom'
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
spacingSize="sm"
|
||||
size="xs"
|
||||
@@ -147,32 +146,34 @@
|
||||
From connection string
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="block text-primary">
|
||||
<div class="w-[550px] flex flex-col items-start gap-1">
|
||||
<div class="flex flex-row gap-1 w-full">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={connectionString}
|
||||
placeholder="postgres://user:password@host:5432/dbname?sslmode=disable"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
on:click={() => {
|
||||
parseConnectionString(close)
|
||||
}}
|
||||
disabled={connectionString.length <= 0}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<div class="block text-primary p-4">
|
||||
<div class="w-[550px] flex flex-col items-start gap-1">
|
||||
<div class="flex flex-row gap-1 w-full">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={connectionString}
|
||||
placeholder="postgres://user:password@host:5432/dbname?sslmode=disable"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
on:click={() => {
|
||||
parseConnectionString(close)
|
||||
}}
|
||||
disabled={connectionString.length <= 0}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
{#if !validConnectionString}
|
||||
<p class="text-red-500 text-xs">Could not parse connection string</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !validConnectionString}
|
||||
<p class="text-red-500 text-xs">Could not parse connection string</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if resourceType == 'postgresql' && supabaseWizard}
|
||||
<a
|
||||
|
||||
@@ -756,14 +756,16 @@
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={oneOfSelected}
|
||||
on:selected={() => {
|
||||
value = { label: oneOfSelected }
|
||||
selected={oneOfSelected}
|
||||
on:selected={({ detail }) => {
|
||||
oneOfSelected = detail
|
||||
value = { label: detail }
|
||||
redraw += 1
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title} label={obj.title} />
|
||||
<ToggleButton value={obj.title ?? ''} label={obj.title} {item} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
{#if oneOfSelected}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button, Popup } from './common'
|
||||
import { Button } from './common'
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import AssignableTagsInner from './AssignableTagsInner.svelte'
|
||||
@@ -11,12 +12,13 @@
|
||||
export let showWorkspaceRestriction = false
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: placement }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
{disabled}
|
||||
closeButton
|
||||
usePointerDownOutside
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button {color} size="xs" nonCaptureEvent={true} {disabled}>
|
||||
<div class="flex flex-row gap-1 items-center"
|
||||
><Pen size={14} /> Custom tags <Tooltip light
|
||||
@@ -28,5 +30,7 @@
|
||||
>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<AssignableTagsInner {showWorkspaceRestriction} on:refresh />
|
||||
</Popup>
|
||||
<svelte:fragment slot="content">
|
||||
<AssignableTagsInner {showWorkspaceRestriction} on:refresh />
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
loadCustomTags()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-72 p-2 gap-2">
|
||||
<div class="flex flex-col w-72 p-4 gap-2">
|
||||
{#if customTags == undefined}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:else}
|
||||
|
||||
@@ -52,9 +52,15 @@
|
||||
<label class="block pb-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={value['custom']}>
|
||||
<ToggleButton value={false} label={'Org'} />
|
||||
<ToggleButton value={true} label="Custom" />
|
||||
<ToggleButtonGroup
|
||||
selected={value['custom'] ? 'custom' : 'org'}
|
||||
on:selected={({ detail }) => {
|
||||
value['custom'] = detail === 'custom'
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="org" label={'Org'} {item} />
|
||||
<ToggleButton value="custom" label="Custom" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="grow">
|
||||
|
||||
@@ -264,28 +264,23 @@
|
||||
on:selected={(e) => dispatch('dirty')}
|
||||
bind:selected={config.integration.type}
|
||||
class="mb-4 mt-2"
|
||||
let:item
|
||||
>
|
||||
<ToggleButton
|
||||
value="dryrun"
|
||||
size="sm"
|
||||
label="Dry run"
|
||||
tooltip="See autoscaling events but not actual scaling actions will be performed"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="script"
|
||||
size="sm"
|
||||
label="Custom script"
|
||||
tooltip="Run a custom script to scale your worker group"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton position="center" disabled value="ecs" size="sm" label="ECS (soon)" />
|
||||
<ToggleButton position="right" disabled value="nomad" size="sm" label="Nomad (soon)" />
|
||||
<ToggleButton
|
||||
position="right"
|
||||
disabled
|
||||
value="kubernetes"
|
||||
size="sm"
|
||||
label="Kubernetes (soon)"
|
||||
/>
|
||||
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
|
||||
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
|
||||
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{#if config.integration.type === 'script'}
|
||||
@@ -347,12 +342,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
|
||||
<ToggleButton value="dryrun" size="sm" label="Dry run" />
|
||||
<ToggleButton value="script" size="sm" label="Custom script" />
|
||||
<ToggleButton position="center" value="ecs" size="sm" label="ECS (soon)" />
|
||||
<ToggleButton position="right" value="nomad" size="sm" label="Nomad (soon)" />
|
||||
<ToggleButton position="right" value="kubernetes" size="sm" label="Kubernetes (soon)" />
|
||||
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2" let:item>
|
||||
<ToggleButton value="dryrun" label="Dry run" {item} />
|
||||
<ToggleButton value="script" label="Custom script" {item} />
|
||||
<ToggleButton value="ecs" label="ECS (soon)" {item} />
|
||||
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
|
||||
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<label>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button, Popup } from './common'
|
||||
import { Button } from './common'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import { autoPlacement } from '@floating-ui/core'
|
||||
import ChangeInstanceUsernameInner from './ChangeInstanceUsernameInner.svelte'
|
||||
|
||||
@@ -8,7 +9,7 @@
|
||||
export let isConflict = false
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
<Popover
|
||||
floatingConfig={{
|
||||
middleware: [
|
||||
autoPlacement({
|
||||
@@ -16,19 +17,20 @@
|
||||
})
|
||||
]
|
||||
}}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
closeButton
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button color={isConflict ? 'red' : 'light'} size="xs" spacingSize="xs2" nonCaptureEvent={true}
|
||||
>{isConflict ? 'Fix username conflict' : 'Change username'}</Button
|
||||
>
|
||||
</svelte:fragment>
|
||||
<ChangeInstanceUsernameInner
|
||||
{email}
|
||||
{username}
|
||||
{isConflict}
|
||||
on:close={() => close(null)}
|
||||
on:renamed
|
||||
/>
|
||||
</Popup>
|
||||
<svelte:fragment slot="content">
|
||||
<ChangeInstanceUsernameInner
|
||||
{email}
|
||||
{username}
|
||||
{isConflict}
|
||||
on:close={() => close()}
|
||||
on:renamed
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let email: string
|
||||
export let username: string
|
||||
export let isConflict = false
|
||||
export let noPadding = false
|
||||
|
||||
let loading = false
|
||||
|
||||
@@ -71,11 +72,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col max-w-2xl p-2">
|
||||
{#if isConflict}
|
||||
<span class="text-sm mb-2 leading-6 font-semibold"
|
||||
>Fix username conflict</span
|
||||
>
|
||||
<div class="flex flex-col max-w-2xl {noPadding ? '' : 'p-4'}">
|
||||
{#if isConflict}
|
||||
<span class="text-sm mb-2 leading-6 font-semibold">Fix username conflict</span>
|
||||
{/if}
|
||||
|
||||
<span class="text-sm font-semibold mb-1 leading-6"
|
||||
|
||||
@@ -157,15 +157,23 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2 flex-wrap sticky top-0 left-0 right-0 bg-surface">
|
||||
<div class="p-2">
|
||||
<ToggleButtonGroup bind:selected={searchKind} class="h-10 ">
|
||||
<ToggleButton small light value="all" label={'All' + counts.all} />
|
||||
<ToggleButton small light value="scripts" icon={Code2} label={'Scripts' + counts.scripts} />
|
||||
<ToggleButtonGroup bind:selected={searchKind} class="h-10 " let:item>
|
||||
<ToggleButton small light value="all" label={'All' + counts.all} {item} />
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="scripts"
|
||||
icon={Code2}
|
||||
label={'Scripts' + counts.scripts}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
light
|
||||
value="resources"
|
||||
icon={Boxes}
|
||||
label={'Resources' + counts.resources}
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
@@ -174,6 +182,7 @@
|
||||
label={'Flows' + counts.flows}
|
||||
icon={FlowIcon}
|
||||
selectedColor="#14b8a6"
|
||||
{item}
|
||||
/>
|
||||
<ToggleButton
|
||||
small
|
||||
@@ -182,6 +191,7 @@
|
||||
label={'Apps' + counts.apps}
|
||||
icon={LayoutDashboard}
|
||||
selectedColor="#fb923c"
|
||||
{item}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Section from './Section.svelte'
|
||||
import { Button, Popup } from './common'
|
||||
import { Button } from './common'
|
||||
import { Clock } from 'lucide-svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Popover floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }} closeButton>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true} startIcon={{ icon: Clock }}>
|
||||
Use simplified builder
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<Section label="CRON Builder">
|
||||
<div class="flex flex-col w-72">
|
||||
<slot {close} />
|
||||
</div>
|
||||
</Section>
|
||||
</Popup>
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<Section label="CRON Builder" wrapperClass="p-4">
|
||||
<div class="flex flex-col w-72">
|
||||
<slot {close} />
|
||||
</div>
|
||||
</Section>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
schedule = nschedule
|
||||
close(null)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
Set cron schedule
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
$: dbSchema = resourcePath && resourcePath in $dbSchemas ? $dbSchemas[resourcePath] : undefined
|
||||
|
||||
$: shouldDisplayError = resourcePath && resourcePath in $dbSchemas && !$dbSchemas[resourcePath]
|
||||
|
||||
function handleSelected({ detail }: CustomEvent<string>) {
|
||||
if (dbSchema && dbSchema.lang !== 'graphql') {
|
||||
dbSchema.publicOnly = detail === 'dbo'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
@@ -86,9 +92,14 @@
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
{#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC || dbSchema.schema?.dbo)}
|
||||
<ToggleButtonGroup class="mb-4" bind:selected={dbSchema.publicOnly}>
|
||||
<ToggleButton value={true} label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} />
|
||||
<ToggleButton value={false} label="All" />
|
||||
<ToggleButtonGroup
|
||||
class="mb-4"
|
||||
selected={dbSchema.publicOnly ? 'dbo' : 'all'}
|
||||
on:selected={handleSelected}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton value="dbo" label={dbSchema.schema.dbo ? 'Dbo' : 'Public'} {item} />
|
||||
<ToggleButton value="all" label="All" {item} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
{#if dbSchema.lang === 'graphql'}
|
||||
|
||||
@@ -146,9 +146,9 @@
|
||||
</Button>
|
||||
{/if}
|
||||
<!-- <div>
|
||||
<ToggleButtonGroup bind:selected={format}>
|
||||
<ToggleButton light small value={'local'} label="local" />
|
||||
<ToggleButton light small value={'utc'} label="utc" />
|
||||
<ToggleButtonGroup bind:selected={format} let:item>
|
||||
<ToggleButton light small value={'local'} label="local" {item} />
|
||||
<ToggleButton light small value={'utc'} label="utc" {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Button, Popup } from './common'
|
||||
import { Button } from './common'
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
|
||||
import DefaultTagsInner from './DefaultTagsInner.svelte'
|
||||
|
||||
@@ -11,11 +12,8 @@
|
||||
let placement: 'bottom-end' | 'top-end' = 'bottom-end'
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: placement }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Popover floatingConfig={{ strategy: 'absolute', placement: placement }} contentClasses="p-4">
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true}>
|
||||
<div class="flex flex-row gap-1 items-center"
|
||||
><Pen size={14} /> Default tags <Tooltip light
|
||||
@@ -25,5 +23,7 @@
|
||||
>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<DefaultTagsInner bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
|
||||
</Popup>
|
||||
<svelte:fragment slot="content">
|
||||
<DefaultTagsInner bind:defaultTagPerWorkspace bind:defaultTagWorkspaces />
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import PdfViewer from './display/PdfViewer.svelte'
|
||||
import type { DisplayResultUi } from './custom_ui'
|
||||
import { getContext, hasContext } from 'svelte'
|
||||
|
||||
export let result: any
|
||||
export let requireHtmlApproval = false
|
||||
@@ -48,6 +50,7 @@
|
||||
export let nodeId: string | undefined = undefined
|
||||
export let language: string | undefined = undefined
|
||||
export let appPath: string | undefined = undefined
|
||||
export let customUi: DisplayResultUi | undefined = undefined
|
||||
|
||||
const IMG_MAX_SIZE = 10000000
|
||||
const TABLE_MAX_SIZE = 5000000
|
||||
@@ -389,6 +392,10 @@
|
||||
let globalForceJson: boolean = false
|
||||
|
||||
let seeS3PreviewFileFromList = ''
|
||||
|
||||
const disableTooltips = hasContext('disableTooltips')
|
||||
? getContext('disableTooltips') === true
|
||||
: false
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -402,10 +409,11 @@
|
||||
on:selected={(ev) => {
|
||||
globalForceJson = ev.detail === 'json'
|
||||
}}
|
||||
let:item
|
||||
>
|
||||
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />
|
||||
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} {item} />
|
||||
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} />
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -438,44 +446,57 @@
|
||||
{#if !hideAsJson && !['json', 's3object'].includes(resultKind ?? '') && typeof result === 'object'}<ToggleButtonGroup
|
||||
class="h-6"
|
||||
selected={forceJson ? 'json' : resultKind?.startsWith('table-') ? 'table' : 'pretty'}
|
||||
let:item
|
||||
on:selected={(ev) => {
|
||||
forceJson = ev.detail === 'json'
|
||||
}}
|
||||
>
|
||||
{#if ['table-col', 'table-row', 'table-row-object'].includes(resultKind ?? '')}
|
||||
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} />
|
||||
<ToggleButton class="px-1.5" value="table" label="Table" icon={Table2} {item} />
|
||||
{:else}
|
||||
<ToggleButton class="px-1.5" value="pretty" label="Pretty" icon={Highlighter} />
|
||||
<ToggleButton
|
||||
class="px-1.5"
|
||||
value="pretty"
|
||||
label="Pretty"
|
||||
icon={Highlighter}
|
||||
{item}
|
||||
/>
|
||||
{/if}
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} />
|
||||
<ToggleButton class="px-1.5" value="json" label="JSON" icon={Braces} {item} />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-secondary text-xs flex gap-2.5 z-10 items-center">
|
||||
<slot name="copilot-fix" />
|
||||
{#if customUi?.disableAiFix !== true}
|
||||
<slot name="copilot-fix" />
|
||||
{/if}
|
||||
{#if !disableExpand && !noControls}
|
||||
<a
|
||||
download="{filename ?? 'result'}.json"
|
||||
class="-mt-1 text-current"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
>
|
||||
<Download size={14} />
|
||||
</a>
|
||||
<Popover
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
|
||||
>
|
||||
<svelte:fragment slot="text">
|
||||
The result renderer in Windmill supports rich display rendering, allowing you to
|
||||
customize the display format of your results.
|
||||
</svelte:fragment>
|
||||
<div class="-mt-1">
|
||||
<InfoIcon size={14} />
|
||||
</div>
|
||||
</Popover>
|
||||
{#if customUi?.disableDownload !== true}
|
||||
<a
|
||||
download="{filename ?? 'result'}.json"
|
||||
class="-mt-1 text-current"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
>
|
||||
<Download size={14} />
|
||||
</a>
|
||||
{/if}
|
||||
{#if disableTooltips !== true}
|
||||
<Popover
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/rich_display_rendering"
|
||||
>
|
||||
<svelte:fragment slot="text">
|
||||
The result renderer in Windmill supports rich display rendering, allowing you to
|
||||
customize the display format of your results.
|
||||
</svelte:fragment>
|
||||
<div class="-mt-1">
|
||||
<InfoIcon size={14} />
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
<button on:click={() => copyToClipboard(toJsonStr(result))} class="-mt-1">
|
||||
<ClipboardCopy size={14} />
|
||||
</button>
|
||||
@@ -885,19 +906,21 @@
|
||||
<Drawer bind:this={jsonViewer} bind:open={drawerOpen} size="900px">
|
||||
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
download="{filename ?? 'result'}.json"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
startIcon={{ icon: Download }}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{#if customUi?.disableDownload !== true}
|
||||
<Button
|
||||
download="{filename ?? 'result'}.json"
|
||||
href={workspaceId && jobId
|
||||
? nodeId
|
||||
? `${base}/api/w/${workspaceId}/jobs/result_by_id/${jobId}/${nodeId}`
|
||||
: `${base}/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
|
||||
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
|
||||
startIcon={{ icon: Download }}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => copyToClipboard(toJsonStr(result))}
|
||||
color="light"
|
||||
|
||||
@@ -1,43 +1,114 @@
|
||||
<script context="module" lang="ts">
|
||||
import { writable } from 'svelte/store'
|
||||
const activeDropdown = writable<{ id: string | null; close: (() => void) | null }>({
|
||||
id: null,
|
||||
close: null
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { MoreVertical } from 'lucide-svelte'
|
||||
import Menu from './common/menu/MenuV2.svelte'
|
||||
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import type { Item } from '$lib/utils'
|
||||
import DropdownV2Inner from './DropdownV2Inner.svelte'
|
||||
|
||||
type Item = {
|
||||
displayName: string
|
||||
action?: (e: CustomEvent<any>) => void
|
||||
icon?: any
|
||||
href?: string
|
||||
disabled?: boolean
|
||||
type?: 'action' | 'delete'
|
||||
hide?: boolean | undefined
|
||||
}
|
||||
import { pointerDownOutside } from '$lib/utils'
|
||||
import { createDropdownMenu, melt, createSync } from '@melt-ui/svelte'
|
||||
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
|
||||
export let justifyEnd: boolean = true
|
||||
export let disabled = false
|
||||
export let placement: Placement = 'bottom-end'
|
||||
export let usePointerDownOutside = false
|
||||
export let closeOnOtherDropdownOpen = true
|
||||
|
||||
const {
|
||||
elements: { menu, item, trigger },
|
||||
states,
|
||||
ids: { menu: dropdownId }
|
||||
} = createDropdownMenu({
|
||||
positioning: {
|
||||
placement
|
||||
},
|
||||
loop: true,
|
||||
onOpenChange: ({ next }) => {
|
||||
if (closeOnOtherDropdownOpen) {
|
||||
if (next) {
|
||||
// Close previous dropdown if exists
|
||||
if ($activeDropdown.close && $activeDropdown.id !== $dropdownId) {
|
||||
$activeDropdown.close()
|
||||
}
|
||||
// Set this dropdown as active
|
||||
activeDropdown.set({ id: $dropdownId, close })
|
||||
} else if ($activeDropdown.id === $dropdownId) {
|
||||
activeDropdown.set({ id: null, close: null })
|
||||
}
|
||||
}
|
||||
return next
|
||||
}
|
||||
})
|
||||
|
||||
let open = false
|
||||
const sync = createSync(states)
|
||||
$: sync.open(open, (v) => (open = Boolean(v)))
|
||||
|
||||
export function close() {
|
||||
open = false
|
||||
}
|
||||
|
||||
async function computeItems(): Promise<Item[]> {
|
||||
if (typeof items === 'function') {
|
||||
return ((await items()) ?? []).filter((item) => !item.hide)
|
||||
const result = await items()
|
||||
return Array.isArray(result) ? result.filter((item) => !item.hide) : []
|
||||
} else {
|
||||
return items.filter((item) => !item.hide)
|
||||
}
|
||||
}
|
||||
async function getMenuElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(document.querySelectorAll('[data-menu]')) as HTMLElement[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu placement="bottom-end" {justifyEnd} on:close on:open {disabled}>
|
||||
<div slot="trigger">
|
||||
{#if $$slots.buttonReplacement}
|
||||
<slot name="buttonReplacement" />
|
||||
{:else}
|
||||
<MoreVertical
|
||||
size={16}
|
||||
class="w-8 h-8 p-2 hover:bg-surface-hover cursor-pointer rounded-md"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<ResolveOpen {open} on:open on:close />
|
||||
|
||||
<DropdownV2Inner items={computeItems} />
|
||||
</Menu>
|
||||
<button
|
||||
class={twMerge('w-full h-8 flex items-center justify-end', $$props.class)}
|
||||
use:melt={$trigger}
|
||||
{disabled}
|
||||
on:click={(e) => e.stopPropagation()}
|
||||
use:pointerDownOutside={{
|
||||
capture: true,
|
||||
stopPropagation: false,
|
||||
exclude: getMenuElements,
|
||||
customEventName: 'pointerdown_menu'
|
||||
}}
|
||||
on:pointerdown_outside={() => {
|
||||
if (usePointerDownOutside) {
|
||||
close()
|
||||
}
|
||||
}}
|
||||
data-menu
|
||||
>
|
||||
{#if $$slots.buttonReplacement}
|
||||
<slot name="buttonReplacement" />
|
||||
{:else}
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
size="xs"
|
||||
color="light"
|
||||
startIcon={{ icon: MoreVertical }}
|
||||
btnClasses="bg-transparent"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div use:melt={$menu} data-menu class="z-[6000]">
|
||||
<div
|
||||
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
|
||||
>
|
||||
<DropdownV2Inner items={computeItems} meltItem={item} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import { MenuItem } from '$lib/components/meltComponents'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
type Item = {
|
||||
displayName: string
|
||||
action?: (e: CustomEvent<any>) => void
|
||||
icon?: any
|
||||
href?: string
|
||||
disabled?: boolean
|
||||
type?: 'action' | 'delete'
|
||||
hide?: boolean | undefined
|
||||
}
|
||||
|
||||
import type { MenubarMenuElements } from '@melt-ui/svelte'
|
||||
import type { Item } from '$lib/utils'
|
||||
export let items: Item[] | (() => Item[]) | (() => Promise<Item[]>) = []
|
||||
export let meltItem: MenubarMenuElements['item']
|
||||
|
||||
let computedItems: Item[] | undefined = undefined
|
||||
async function computeItems() {
|
||||
@@ -35,16 +27,18 @@
|
||||
href={item?.href}
|
||||
disabled={item?.disabled}
|
||||
class={twMerge(
|
||||
'px-4 py-2 text-primary hover:bg-surface-hover hover:text-primary cursor-pointer text-xs transition-all',
|
||||
'px-4 py-2 text-primary font-semibold hover:bg-surface-hover cursor-pointer text-xs transition-all',
|
||||
'data-[highlighted]:bg-surface-hover',
|
||||
'flex flex-row gap-2 items-center',
|
||||
item?.disabled && 'text-gray-400 cursor-not-allowed',
|
||||
item?.type === 'delete' &&
|
||||
!item?.disabled &&
|
||||
'text-red-500 hover:bg-red-100 hover:text-red-500'
|
||||
'text-red-500 hover:bg-red-100 hover:text-red-500 data-[highlighted]:text-red-500 data-[highlighted]:bg-red-100'
|
||||
)}
|
||||
item={meltItem}
|
||||
>
|
||||
{#if item.icon}
|
||||
<svelte:component this={item.icon} size={14} />
|
||||
<svelte:component this={item.icon} size={14} color={item.iconColor} />
|
||||
{/if}
|
||||
{item.displayName}
|
||||
</MenuItem>
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import Popup from './common/popup/Popup.svelte'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { tweened } from 'svelte/motion'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
|
||||
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
@@ -53,6 +54,7 @@
|
||||
export let disableDnd: boolean = false
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
export let isValid: boolean = true
|
||||
export let customUi: EditableSchemaFormUi | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -201,7 +203,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let jsonView: boolean = false
|
||||
let jsonView: boolean = customUi?.jsonOnly == true
|
||||
let schemaString: string = JSON.stringify(schema, null, '\t')
|
||||
let error: string | undefined = undefined
|
||||
let editor: SimpleEditor | undefined = undefined
|
||||
@@ -307,6 +309,7 @@
|
||||
}}
|
||||
{shouldDispatchChanges}
|
||||
bind:isValid
|
||||
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
|
||||
/>
|
||||
|
||||
<slot name="runButton" />
|
||||
@@ -325,7 +328,7 @@
|
||||
<slot name="extraTab" />
|
||||
{:else}
|
||||
<!-- WIP -->
|
||||
{#if jsonEnabled}
|
||||
{#if jsonEnabled && customUi?.jsonOnly != true}
|
||||
<div class="w-full p-3 flex justify-end">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
@@ -371,12 +374,8 @@
|
||||
{argName}
|
||||
{#if !uiOnly}
|
||||
<div on:click|stopPropagation|preventDefault>
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Popover placement="bottom-end" containerClasses="p-4" closeButton>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
@@ -385,34 +384,36 @@
|
||||
iconOnly
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<Label label="Name">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
value={argName}
|
||||
id={argName + i}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
<svelte:fragment slot="content" let:close>
|
||||
<Label label="Name" class="p-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
value={argName}
|
||||
id={argName + i}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
renameProperty(argName, argName + i)
|
||||
close()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</Popup>
|
||||
close()
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -467,6 +468,7 @@
|
||||
<ToggleButtonGroup
|
||||
tabListClass="flex-wrap"
|
||||
class="h-auto"
|
||||
let:item
|
||||
bind:selected
|
||||
on:selected={(e) => {
|
||||
const isS3 = e.detail == 'S3'
|
||||
@@ -547,7 +549,7 @@
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} />
|
||||
<ToggleButton value={x[1]} label={x[0]} {item} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
@@ -593,7 +593,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if showResourcePicker}
|
||||
{#if showResourcePicker && customUi?.resource != false}
|
||||
<Button
|
||||
title="Add resource"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
@@ -623,18 +623,20 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
title="Reset Content"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
color="light"
|
||||
on:click={clearContent}
|
||||
{iconOnly}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
{#if customUi?.reset != false}
|
||||
<Button
|
||||
title="Reset Content"
|
||||
btnClasses="!font-medium text-tertiary"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
color="light"
|
||||
on:click={clearContent}
|
||||
{iconOnly}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if customUi?.assistants != false}
|
||||
{#if lang == 'deno' || lang == 'python3' || lang == 'go' || lang == 'bash'}
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
import { Settings } from 'lucide-svelte'
|
||||
import FormatOnSave from './FormatOnSave.svelte'
|
||||
import VimMode from './VimMode.svelte'
|
||||
import { Button, Popup } from './common'
|
||||
import { Button } from './common'
|
||||
import CodeCompletionStatus from './copilot/CodeCompletionStatus.svelte'
|
||||
import type { EditorBarUi } from './custom_ui'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
|
||||
export let customUi: EditorBarUi = {}
|
||||
</script>
|
||||
|
||||
{#if customUi?.autoformatting != false || customUi?.vimMode != false || customUi?.aiCompletion != false}
|
||||
<Popup
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
usePointerDownOutside
|
||||
contentClasses="flex flex-col gap-y-2 p-4"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button
|
||||
btnClasses="text-tertiary"
|
||||
color="light"
|
||||
@@ -26,7 +28,7 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<svelte:fragment slot="content">
|
||||
{#if customUi?.autoformatting != false}
|
||||
<div>
|
||||
<FormatOnSave />
|
||||
@@ -42,6 +44,6 @@
|
||||
<CodeCompletionStatus />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Popup>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
Pen,
|
||||
Save,
|
||||
DiffIcon,
|
||||
MoreVertical,
|
||||
HistoryIcon,
|
||||
FileJson,
|
||||
type Icon,
|
||||
@@ -73,19 +72,17 @@
|
||||
import { loadFlowModuleState, pickScript } from './flows/flowStateUtils'
|
||||
import FlowCopilotInputsModal from './copilot/FlowCopilotInputsModal.svelte'
|
||||
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
|
||||
|
||||
import Dropdown from '$lib/components/DropdownV2.svelte'
|
||||
import FlowTutorials from './FlowTutorials.svelte'
|
||||
import { ignoredTutorials } from './tutorials/ignoredTutorials'
|
||||
import type DiffDrawer from './DiffDrawer.svelte'
|
||||
import FlowHistory from './flows/FlowHistory.svelte'
|
||||
import ButtonDropdown from './common/button/ButtonDropdown.svelte'
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import CustomPopover from './CustomPopover.svelte'
|
||||
import Summary from './Summary.svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
|
||||
import { type TriggerContext, type ScheduleTrigger } from './triggers'
|
||||
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let pathStoreInit: string | undefined = undefined
|
||||
@@ -116,6 +113,12 @@
|
||||
|
||||
$: setContext('customUi', customUi)
|
||||
|
||||
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
|
||||
return {
|
||||
savedValue: savedFlow,
|
||||
modifiedValue: $flowStore
|
||||
}
|
||||
}
|
||||
let onLatest = true
|
||||
async function compareVersions() {
|
||||
if (version === undefined) {
|
||||
@@ -1364,33 +1367,7 @@
|
||||
{/if}
|
||||
<div>
|
||||
{#if moreItems?.length > 0}
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<Button nonCaptureEvent size="xs" color="light">
|
||||
<div class="flex flex-row items-center">
|
||||
<MoreVertical size={14} />
|
||||
</div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
{#each moreItems as item}
|
||||
<MenuItem
|
||||
on:click={item.action}
|
||||
disabled={item.disabled}
|
||||
class={item.disabled ? 'opacity-50' : ''}
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-primary flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
<svelte:component this={item.icon} size={14} />
|
||||
{item.displayName}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
<Dropdown items={moreItems} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if customUi?.topBar?.tutorials != false}
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { BookOpen } from 'lucide-svelte'
|
||||
import ButtonDropdown from './common/button/ButtonDropdown.svelte'
|
||||
import { BookOpen, CheckCircle, Circle, RefreshCw, CheckCheck } from 'lucide-svelte'
|
||||
import Dropdown from '$lib/components/DropdownV2.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
|
||||
import MenuItem from './common/menu/MenuItem.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { resetAllTodos, skipAllTodos } from '$lib/tutorialUtils'
|
||||
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
|
||||
import TutorialItem from './tutorials/TutorialItem.svelte'
|
||||
import FlowTutorials from './FlowTutorials.svelte'
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
|
||||
let targetTutorial: string | undefined = undefined
|
||||
let flowTutorials: FlowTutorials | undefined = undefined
|
||||
|
||||
async function getTutorialItems() {
|
||||
const tutorials = [
|
||||
{ displayName: 'Simple flow tutorials', id: 'action' },
|
||||
{ displayName: 'For loops tutorial', id: 'forloop' },
|
||||
{ displayName: 'Branch one tutorial', id: 'branchone' },
|
||||
{ displayName: 'Branch all tutorial', id: 'branchall' },
|
||||
{ displayName: 'Error handler', id: 'error-handler' }
|
||||
]
|
||||
|
||||
return [
|
||||
...tutorials.map((tutorial, index) => ({
|
||||
displayName: tutorial.displayName,
|
||||
action: () => flowTutorials?.runTutorialById(tutorial.id),
|
||||
icon: $tutorialsToDo.includes(index) ? Circle : CheckCircle,
|
||||
iconColor: $tutorialsToDo.includes(index) ? undefined : 'green'
|
||||
})),
|
||||
{
|
||||
displayName: 'Skip tutorials',
|
||||
action: () => skipAllTodos(),
|
||||
icon: CheckCheck
|
||||
},
|
||||
{
|
||||
displayName: 'Reset tutorials',
|
||||
action: () => resetAllTodos(),
|
||||
icon: RefreshCw
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:pointerdown|stopPropagation>
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
{#key $tutorialsToDo}
|
||||
<Dropdown items={getTutorialItems} class="w-fit">
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
@@ -26,63 +52,8 @@
|
||||
startIcon={{ icon: BookOpen }}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
<TutorialItem
|
||||
on:click={() => flowTutorials?.runTutorialById('action')}
|
||||
label="Simple flow tutorial"
|
||||
index={0}
|
||||
/>
|
||||
|
||||
<TutorialItem
|
||||
on:click={() => flowTutorials?.runTutorialById('forloop')}
|
||||
label="For loops tutorial"
|
||||
index={1}
|
||||
/>
|
||||
|
||||
<TutorialItem
|
||||
on:click={() => flowTutorials?.runTutorialById('branchone')}
|
||||
label="Branch one tutorial"
|
||||
index={2}
|
||||
/>
|
||||
|
||||
<TutorialItem
|
||||
on:click={() => flowTutorials?.runTutorialById('branchall')}
|
||||
label="Branch all tutorial"
|
||||
index={3}
|
||||
/>
|
||||
|
||||
<TutorialItem
|
||||
on:click={() => flowTutorials?.runTutorialById('error-handler')}
|
||||
label="Error handler"
|
||||
index={4}
|
||||
/>
|
||||
|
||||
<div class="border-t border-surface-hover" />
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
resetAllTodos()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
Reset tutorials
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => skipAllTodos()}>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
Skip tutorials
|
||||
</div>
|
||||
</MenuItem>
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
</button>
|
||||
</Dropdown>
|
||||
{/key}
|
||||
|
||||
<FlowTutorials
|
||||
bind:this={flowTutorials}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { type Job, JobService, type Flow, type RestartedFrom, type OpenFlow } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button, Popup } from './common'
|
||||
import { Badge, Button } from './common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { runFlowPreview } from './flows/utils'
|
||||
@@ -17,6 +18,7 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import JsonInputs from './JsonInputs.svelte'
|
||||
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
|
||||
import { NEVER_TESTED_THIS_FAR } from './flows/models'
|
||||
|
||||
export let previewMode: 'upTo' | 'whole'
|
||||
export let open: boolean
|
||||
@@ -171,8 +173,11 @@
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
async function loadIndividualStepsStates() {
|
||||
// console.log('loadIndividualStepsStates')
|
||||
dfs($flowStore.value.modules, async (module) => {
|
||||
if ($flowStateStore[module.id]?.previewResult) {
|
||||
// console.log('module', $flowStateStore[module.id], module.id)
|
||||
const prev = $flowStateStore[module.id]?.previewResult
|
||||
if (prev && prev != NEVER_TESTED_THIS_FAR) {
|
||||
return
|
||||
}
|
||||
const previousJobId = await JobService.listJobs({
|
||||
@@ -182,6 +187,7 @@
|
||||
page: 1,
|
||||
perPage: 1
|
||||
})
|
||||
// console.log('previousJobId', previousJobId, module.id)
|
||||
|
||||
if (previousJobId.length > 0) {
|
||||
const getJobResult = await JobService.getCompletedJobResultMaybe({
|
||||
@@ -261,7 +267,10 @@
|
||||
</Badge>
|
||||
</Button>
|
||||
{:else}
|
||||
<Popup floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}>
|
||||
<Popover
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}
|
||||
contentClasses="p-4"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
title={`Re-start this flow from step ${selectedJobStep} (included).`}
|
||||
@@ -283,49 +292,51 @@
|
||||
</Badge>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<label class="block text-primary">
|
||||
<div class="pb-1 text-sm text-secondary"
|
||||
>{selectedJobStepType == 'forloop' ? 'From iteration #:' : 'From branch:'}</div
|
||||
>
|
||||
<div class="flex w-full">
|
||||
{#if selectedJobStepType === 'forloop'}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
/>
|
||||
{:else}
|
||||
<select
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
>
|
||||
{#each restartBranchNames as [branchIdx, branchName]}
|
||||
<option value={branchIdx}>{branchName}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Restart flow"
|
||||
on:click|once={() => {
|
||||
runPreview($previewArgs, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: branchOrIterationN
|
||||
})
|
||||
}}
|
||||
<svelte:fragment slot="content">
|
||||
<label class="block text-primary p-4">
|
||||
<div class="pb-1 text-sm text-secondary"
|
||||
>{selectedJobStepType == 'forloop' ? 'From iteration #:' : 'From branch:'}</div
|
||||
>
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
</Popup>
|
||||
<div class="flex w-full">
|
||||
{#if selectedJobStepType === 'forloop'}
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
/>
|
||||
{:else}
|
||||
<select
|
||||
bind:value={branchOrIterationN}
|
||||
class="!w-32 grow"
|
||||
on:click|stopPropagation={() => {}}
|
||||
>
|
||||
{#each restartBranchNames as [branchIdx, branchName]}
|
||||
<option value={branchIdx}>{branchName}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs"
|
||||
color="blue"
|
||||
buttonType="button"
|
||||
btnClasses="!p-1 !w-[34px] !ml-1"
|
||||
aria-label="Restart flow"
|
||||
on:click|once={() => {
|
||||
runPreview($previewArgs, {
|
||||
flow_job_id: jobId,
|
||||
step_id: selectedJobStep,
|
||||
branch_or_iteration_n: branchOrIterationN
|
||||
})
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user