Merge branch 'main' into python-uv

This commit is contained in:
pyranota
2024-10-17 13:18:58 +00:00
committed by GitHub
187 changed files with 11232 additions and 3152 deletions
+6 -21
View File
@@ -1,22 +1,6 @@
ARG DEBIAN_IMAGE=debian:bookworm-slim
ARG RUST_IMAGE=rust:1.80-slim-bookworm
ARG PYTHON_IMAGE=python:3.11.4-slim-bookworm
FROM ${DEBIAN_IMAGE} as downloader
ARG TARGETPLATFORM
SHELL ["/bin/bash", "-c"]
RUN apt update -y
RUN apt install -y unzip curl
RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.46.3/deno-x86_64-unknown-linux-gnu.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.46.3/deno-aarch64-unknown-linux-gnu.zip -o deno.zip || true
RUN unzip deno.zip && rm deno.zip
FROM ${RUST_IMAGE} as builder
@@ -31,7 +15,7 @@ ENV SQLX_OFFLINE=true
RUN mkdir -p /frontend/build
RUN apt-get update \
&& apt-get install -y ca-certificates tzdata libpq5 cmake\
&& apt-get install -y ca-certificates tzdata libpq5 cmake unzip\
make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libxml2-dev \
libxmlsec1-dev libffi-dev liblzma-dev mecab-ipadic-utf8 libgdbm-dev libc6-dev git libprotobuf-dev libnl-route-3-dev \
@@ -56,13 +40,14 @@ RUN wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VER
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=oven/bun:1.1.27 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.30 /usr/local/bin/bun /usr/bin/bun
ARG TARGETPLATFORM
RUN [ "$TARGETPLATFORM" == "linux/amd64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.41.0/deno-x86_64-unknown-linux-gnu.zip -o deno.zip || true
RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v1.41.0/deno-aarch64-unknown-linux-gnu.zip -o deno.zip || true
RUN curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.0/deno-x86_64-unknown-linux-gnu.zip -o deno.zip
# RUN [ "$TARGETPLATFORM" == "linux/arm64" ] && curl -Lsf https://github.com/denoland/deno/releases/download/v2.0.0/deno-aarch64-unknown-linux-gnu.zip -o deno.zip || true
COPY --from=downloader --chmod=755 /deno /usr/bin/deno
RUN unzip deno.zip && rm deno.zip && mv deno /usr/bin/deno
RUN apt-get update \
&& apt-get install -y postgresql-client --allow-unauthenticated
+4
View File
@@ -41,6 +41,10 @@ jobs:
- name: cargo test
timeout-minutes: 15
run:
/usr/bin/deno --version &&
/usr/bin/bun -v &&
go version &&
/usr/local/bin/python3 --version &&
mkdir frontend/build && cd backend && touch
windmill-api/openapi-deref.yaml &&
DATABASE_URL=postgres://postgres:changeme@postgres:5432/windmill
@@ -0,0 +1,128 @@
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
name: Build and publish windmill for RHEL9
on:
workflow_dispatch
permissions: write-all
jobs:
build_ee:
runs-on: ubicloud
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
repository: windmill-labs/windmill-ee-private
path: ./windmill-ee-private
ref: ${{ env.ee_repo_ref }}
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
id: meta-ee-public
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-rhel9
flavor: |
latest=false
tags: |
type=sha
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Substitute EE code
run: |
./backend/substitute_ee_code.sh --copy --dir ./windmill-ee-private
- name: Copy RHEL9 Dockerfile
run: |
cp ./docker/RHEL9/Dockerfile ./Dockerfile
- name: Build and push publicly ee amd64
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
tags: |
${{ steps.meta-ee-public.outputs.tags }}-amd64
labels: |
${{ steps.meta-ee-public.outputs.labels }}-amd64
org.opencontainers.image.licenses=Windmill-Enterprise-License
- name: Build and push publicly ee arm64
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,deno_core
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
tags: |
${{ steps.meta-ee-public.outputs.tags }}-arm64
labels: |
${{ steps.meta-ee-public.outputs.labels }}-arm64
org.opencontainers.image.licenses=Windmill-Enterprise-License
- uses: shrink/actions-docker-extract@v3
id: extract-ee-amd64
with:
image: ${{ steps.meta-ee-public.outputs.tags}}-amd64
path: "/windmill/target/release/windmill"
- uses: shrink/actions-docker-extract@v3
id: extract-ee-arm64
with:
image: ${{ steps.meta-ee-public.outputs.tags}}-arm64
path: "/windmill/target/release/windmill"
- name: Rename binary with corresponding architecture
run: |
mv "${{ steps.extract-ee-amd64.outputs.destination }}/windmill" "${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9"
mv "${{ steps.extract-ee-arm64.outputs.destination }}/windmill" "${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9"
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 build
path: ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9
- uses: actions/upload-artifact@v4
with:
name: RHEL9-arm64 build
path: ${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9
# - name: Attach binary to release
# uses: softprops/action-gh-release@v2
# if: startsWith(github.ref, 'refs/tags/')
# with:
# files: |
# ${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9
# ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9
+1 -11
View File
@@ -394,7 +394,7 @@ jobs:
verify_ee_image_vulnerabilities:
runs-on: ubicloud
needs: [tag_latest_ee]
# if: ${{ startsWith(github.ref, 'refs/tags/') }}
if: ${{ startsWith(github.ref, 'refs/tags/') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -589,8 +589,6 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-cuda
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -635,8 +633,6 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-slim
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -680,8 +676,6 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-slim
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -726,8 +720,6 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-full
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
@@ -771,8 +763,6 @@ jobs:
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee-full
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
+65
View File
@@ -1,5 +1,70 @@
# Changelog
## [1.409.2](https://github.com/windmill-labs/windmill/compare/v1.409.1...v1.409.2) (2024-10-16)
### Bug Fixes
* add extra args support for exception to bun scripts ([1466da3](https://github.com/windmill-labs/windmill/commit/1466da3999add0238b9c42ca13df52194f082fc0))
* fix script persistence in url + add support for extra error args in python ([3174024](https://github.com/windmill-labs/windmill/commit/3174024d8e6ecbe9f8c9e1ea055d611f652c0057))
## [1.409.1](https://github.com/windmill-labs/windmill/compare/v1.409.0...v1.409.1) (2024-10-16)
### Bug Fixes
* **apidocs:** fix generated openapi files ([d24e153](https://github.com/windmill-labs/windmill/commit/d24e1530655d27ffda9bb4c19471dc1431124cc2))
* **git-sync:** propagate update of folders with git sync ([6abb346](https://github.com/windmill-labs/windmill/commit/6abb346013da4a907a860713a8a67642985b8025))
## [1.409.0](https://github.com/windmill-labs/windmill/compare/v1.408.1...v1.409.0) (2024-10-16)
### Features
* **frontend:** unify all triggers UX and simplify flow settings ([#4259](https://github.com/windmill-labs/windmill/issues/4259)) ([91a3d06](https://github.com/windmill-labs/windmill/commit/91a3d065298cce7a882464fa0cd31d8f1ae9dda2))
* Scroll to element in virtual list when clicking on graph point ([#4532](https://github.com/windmill-labs/windmill/issues/4532)) ([7126ba1](https://github.com/windmill-labs/windmill/commit/7126ba12c7eb52d2cfbe8d83311b5592a5707bce))
* **sso:** adding the ability to define a custom display name for sso ([#4529](https://github.com/windmill-labs/windmill/issues/4529)) ([99c5b3e](https://github.com/windmill-labs/windmill/commit/99c5b3ecdacb1158c2cba5b891c4c3b8b70c3b6a))
### Bug Fixes
* Add indexer backup lock to fit the deployment model ([#4531](https://github.com/windmill-labs/windmill/issues/4531)) ([411bce7](https://github.com/windmill-labs/windmill/commit/411bce7e13aabfa53db80d55261ea4511f6d1ae9))
* **app:** accept connecting to non yet existing state output for convenience ([9eb1ecc](https://github.com/windmill-labs/windmill/commit/9eb1ecc9f3017e2f4284a827a2bcd21f36b1b8ac))
* **app:** improve absolute url handling in download button and downloadFile ([dcdbf1a](https://github.com/windmill-labs/windmill/commit/dcdbf1afb4d5a18e00b9bbb1eb0bef129ea5667f))
* **app:** make s3 uploads persistent across tabs change ([c3b536b](https://github.com/windmill-labs/windmill/commit/c3b536b1b8069898131768867a187b186b21e537))
* canceled jobs button reporting 0 jobs cancelled ([#4534](https://github.com/windmill-labs/windmill/issues/4534)) ([e736572](https://github.com/windmill-labs/windmill/commit/e736572db10929ae5e123c4f6cef73b8e90fc29b))
* **python-client:** improve get_job_status for running jobs ([a8c4ea2](https://github.com/windmill-labs/windmill/commit/a8c4ea2334d2535fa7d5d43f58d65565afe8f3e5))
* **ui:** dark mode support for queue metrics based critical alert ([#4535](https://github.com/windmill-labs/windmill/issues/4535)) ([f38b3d1](https://github.com/windmill-labs/windmill/commit/f38b3d14e8092ae58817511aea91a4e77725ead6))
## [1.408.1](https://github.com/windmill-labs/windmill/compare/v1.408.0...v1.408.1) (2024-10-12)
### Bug Fixes
* fix deno cache --allow-import on deno 2 ([42fe31f](https://github.com/windmill-labs/windmill/commit/42fe31f804c9e6643cd90167494e45270831e013))
## [1.408.0](https://github.com/windmill-labs/windmill/compare/v1.407.2...v1.408.0) (2024-10-12)
### Features
* **app builder:** file download helper ([#4511](https://github.com/windmill-labs/windmill/issues/4511)) ([f82f091](https://github.com/windmill-labs/windmill/commit/f82f09129096cfff975370d8bb7b6d832a2b8f9f))
### Bug Fixes
* **cli:** handle case where 'toString' is a schema field ([568cc66](https://github.com/windmill-labs/windmill/commit/568cc66932fb0470f5e89de7b02d94dba4050638))
* **frontend:** s3 file uploader works on public apps too ([982dde2](https://github.com/windmill-labs/windmill/commit/982dde2b9dfe6d9eda300c683af729d97a03cb4d))
* **frontend:** set unused schema property fields to null ([be11240](https://github.com/windmill-labs/windmill/commit/be112408e7c4601314726e9517c37daaeaa1bf09))
* improve workflow as code row-lock on db to handle more concurrency ([d2c4d3f](https://github.com/windmill-labs/windmill/commit/d2c4d3fa207379cb0b8ac180f6ccc759045580e8))
## [1.407.2](https://github.com/windmill-labs/windmill/compare/v1.407.1...v1.407.2) (2024-10-10)
### Bug Fixes
* improve default properties of new nodes of flows (suspend, branchone, branchall) ([d9bdc5a](https://github.com/windmill-labs/windmill/commit/d9bdc5a5b08dd4d0381304656af097315398c9d4))
## [1.407.1](https://github.com/windmill-labs/windmill/compare/v1.407.0...v1.407.1) (2024-10-10)
+1 -1
View File
@@ -177,7 +177,7 @@ COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=denoland/deno:2.0.0 --chmod=755 /usr/bin/deno /usr/bin/deno
COPY --from=oven/bun:1.1.27 /usr/local/bin/bun /usr/bin/bun
COPY --from=oven/bun:1.1.30 /usr/local/bin/bun /usr/bin/bun
COPY --from=php:8.3.7-cli /usr/local/bin/php /usr/bin/php
COPY --from=composer:2.7.6 /usr/bin/composer /usr/bin/composer
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO concurrency_locks (id, last_locked_at, owner)\n VALUES ($1, now(), $2)\n ON CONFLICT (id)\n DO UPDATE SET\n last_locked_at = now(),\n owner = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "14abf759dae7ba5c38017ba6001927c6df0653a02b87bcea939066e39ebcf24d"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool",
"Text"
]
},
"nullable": [
null
]
},
"hash": "1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Bool",
"Text"
]
},
"nullable": [
null
]
},
"hash": "31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE concurrency_locks SET\n last_locked_at = now()\n WHERE id = $1 AND owner = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "57e270e032e8c04dda7b5c1ca949861756b3ad367a4a500728332a7cb91560a4"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT owner FROM concurrency_locks WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "owner",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "5fd70c70ce52cbc51fa9124cb05f82b5951f17d1b7eade53c6d89253d55f8b9f"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8"
}
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "schedule",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Bool",
"Text"
]
},
"nullable": [
false
]
},
"hash": "c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes)\n VALUES ($1, $2, $3, $4, $5, $6)",
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
"describe": {
"columns": [],
"parameters": {
@@ -10,10 +10,11 @@
"Varchar",
"Timestamptz",
"Bool",
"TextArray"
"TextArray",
"Varchar"
]
},
"nullable": []
},
"hash": "34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5"
"hash": "c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c"
}
@@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "token_prefix",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "expiration",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "last_used_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "scopes",
"type_info": "TextArray"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
null,
true,
false,
false,
true,
true
]
},
"hash": "c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT last_locked_at\n FROM concurrency_locks\n WHERE id = $1\n FOR UPDATE",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "last_locked_at",
"type_info": "Timestamp"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "cecf1addc4aecb087a14786b2a9165895ca61ef042947c7314f66514d7f29edc"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66"
}
@@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "label",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "token_prefix",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "expiration",
"type_info": "Timestamptz"
},
{
"ordinal": 3,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "last_used_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "scopes",
"type_info": "TextArray"
},
{
"ordinal": 6,
"name": "email",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
true,
null,
true,
false,
false,
true,
true
]
},
"hash": "eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb"
}
+122 -108
View File
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "Inflector"
@@ -486,9 +486,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.13"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429"
checksum = "103db485efc3e41214fe4fda9f3dbeae2eb9082f48fd236e6095627a9422066e"
dependencies = [
"brotli 7.0.0",
"bzip2",
@@ -650,7 +650,7 @@ dependencies = [
"hex",
"hmac",
"http-types",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-tls 0.5.0",
"serde",
"serde_json",
@@ -794,14 +794,14 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"tracing",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
name = "aws-sdk-sso"
version = "1.45.0"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e33ae899566f3d395cbf42858e433930682cc9c1889fa89318896082fef45efb"
checksum = "0dc2faec3205d496c7e57eff685dd944203df7ce16a4116d0281c44021788a7b"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -821,9 +821,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.46.0"
version = "1.47.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f39c09e199ebd96b9f860b0fce4b6625f211e064ad7c8693b72ecf7ef03881e0"
checksum = "c93c241f52bc5e0476e259c953234dab7e2a35ee207ee202e86c0095ec4951dc"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -843,9 +843,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.45.0"
version = "1.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d95f93a98130389eb6233b9d615249e543f6c24a68ca1f109af9ca5164a8765"
checksum = "b259429be94a3459fa1b00c5684faee118d74f9577cc50aebadc36e507c63b5f"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -954,7 +954,7 @@ dependencies = [
"http-body 0.4.6",
"http-body 1.0.1",
"httparse",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"once_cell",
"pin-project-lite",
@@ -1043,7 +1043,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
"hyper 1.5.0",
"hyper-util",
"itoa",
"matchit",
@@ -1469,9 +1469,9 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
dependencies = [
"bytemuck_derive",
]
@@ -1599,9 +1599,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.28"
version = "1.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1"
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
dependencies = [
"jobserver",
"libc",
@@ -2233,7 +2233,7 @@ dependencies = [
"arrow-array",
"arrow-ipc",
"arrow-schema",
"async-compression 0.4.13",
"async-compression 0.4.16",
"async-trait",
"bytes",
"bzip2",
@@ -2271,7 +2271,7 @@ dependencies = [
"tokio",
"tokio-util",
"url",
"uuid 1.10.0",
"uuid 1.11.0",
"xz2",
"zstd 0.13.2",
]
@@ -2371,7 +2371,7 @@ dependencies = [
"regex",
"sha2 0.10.8",
"unicode-segmentation",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -2538,7 +2538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -2645,7 +2645,7 @@ dependencies = [
"error_reporter",
"http 1.1.0",
"http-body-util",
"hyper 1.4.1",
"hyper 1.5.0",
"hyper-rustls 0.27.3",
"hyper-util",
"ipnet",
@@ -2777,7 +2777,7 @@ checksum = "22a1abc6bd8af41aa2496ceceef94f4277092c781a9495c437327a17659553a2"
dependencies = [
"deno_core",
"deno_native_certs",
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-pemfile 2.2.0",
"rustls-tokio-stream",
"rustls-webpki 0.102.8",
@@ -2822,7 +2822,7 @@ dependencies = [
"futures",
"serde",
"tokio",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -3617,7 +3617,7 @@ dependencies = [
"async-trait",
"base64 0.21.7",
"dirs-next",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"ring 0.16.20",
"rustls 0.21.12",
@@ -4196,9 +4196,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.30"
version = "0.14.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
dependencies = [
"bytes",
"futures-channel",
@@ -4220,9 +4220,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.4.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
dependencies = [
"bytes",
"futures-channel",
@@ -4247,7 +4247,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.30",
"hyper 0.14.31",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
@@ -4263,9 +4263,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.4.1",
"hyper 1.5.0",
"hyper-util",
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-native-certs 0.8.0",
"rustls-pki-types",
"tokio",
@@ -4280,7 +4280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.30",
"hyper 0.14.31",
"native-tls",
"tokio",
"tokio-native-tls",
@@ -4294,7 +4294,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.4.1",
"hyper 1.5.0",
"hyper-util",
"native-tls",
"tokio",
@@ -4313,7 +4313,7 @@ dependencies = [
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"hyper 1.4.1",
"hyper 1.5.0",
"pin-project-lite",
"socket2 0.5.7",
"tokio",
@@ -4889,7 +4889,7 @@ dependencies = [
"base64 0.22.1",
"gethostname",
"mail-builder",
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-pki-types",
"smtp-proto",
"tokio",
@@ -5223,7 +5223,7 @@ dependencies = [
"subprocess",
"thiserror",
"time",
"uuid 1.10.0",
"uuid 1.11.0",
"zstd 0.13.2",
]
@@ -5438,7 +5438,7 @@ dependencies = [
"chrono",
"futures",
"humantime",
"hyper 1.4.1",
"hyper 1.5.0",
"itertools 0.13.0",
"md-5 0.10.6",
"parking_lot",
@@ -5541,9 +5541,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.66"
version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
@@ -5573,9 +5573,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.103"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
@@ -5742,9 +5742,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
[[package]]
name = "pem"
@@ -6021,7 +6021,7 @@ dependencies = [
"postgres-protocol",
"serde",
"serde_json",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -6132,9 +6132,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.87"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
@@ -6365,7 +6365,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.0.0",
"rustls 0.23.14",
"rustls 0.23.15",
"socket2 0.5.7",
"thiserror",
"tokio",
@@ -6382,7 +6382,7 @@ dependencies = [
"rand 0.8.5",
"ring 0.17.8",
"rustc-hash 2.0.0",
"rustls 0.23.14",
"rustls 0.23.15",
"slab",
"thiserror",
"tinyvec",
@@ -6690,7 +6690,7 @@ dependencies = [
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper 0.14.31",
"hyper-rustls 0.24.2",
"hyper-tls 0.5.0",
"ipnet",
@@ -6728,7 +6728,7 @@ version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"async-compression 0.4.13",
"async-compression 0.4.16",
"base64 0.22.1",
"bytes",
"encoding_rs",
@@ -6738,7 +6738,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.4.1",
"hyper 1.5.0",
"hyper-rustls 0.27.3",
"hyper-tls 0.6.0",
"hyper-util",
@@ -6751,7 +6751,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-native-certs 0.8.0",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
@@ -6844,7 +6844,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -7051,9 +7051,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.14"
version = "0.23.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
dependencies = [
"log",
"once_cell",
@@ -7122,9 +7122,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-tokio-stream"
@@ -7133,7 +7133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
dependencies = [
"futures",
"rustls 0.23.14",
"rustls 0.23.15",
"socket2 0.5.7",
"tokio",
]
@@ -7214,9 +7214,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]]
name = "ryu"
@@ -7264,7 +7264,7 @@ dependencies = [
"serde",
"thiserror",
"url",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -7302,7 +7302,7 @@ dependencies = [
"schemars_derive",
"serde",
"serde_json",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -8016,7 +8016,7 @@ dependencies = [
"once_cell",
"paste",
"percent-encoding",
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
@@ -8028,7 +8028,7 @@ dependencies = [
"tokio-stream",
"tracing",
"url",
"uuid 1.10.0",
"uuid 1.11.0",
"webpki-roots 0.26.6",
]
@@ -8112,7 +8112,7 @@ dependencies = [
"stringprep",
"thiserror",
"tracing",
"uuid 1.10.0",
"uuid 1.11.0",
"whoami",
]
@@ -8154,7 +8154,7 @@ dependencies = [
"stringprep",
"thiserror",
"tracing",
"uuid 1.10.0",
"uuid 1.11.0",
"whoami",
]
@@ -8180,7 +8180,7 @@ dependencies = [
"sqlx-core",
"tracing",
"url",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -8833,7 +8833,7 @@ dependencies = [
"tempfile",
"thiserror",
"time",
"uuid 1.10.0",
"uuid 1.11.0",
"winapi",
]
@@ -9041,7 +9041,7 @@ dependencies = [
"tokio-rustls 0.24.1",
"tokio-util",
"tracing",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
@@ -9272,7 +9272,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-pki-types",
"tokio",
]
@@ -9472,7 +9472,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97"
dependencies = [
"async-compression 0.4.13",
"async-compression 0.4.16",
"bitflags 2.6.0",
"bytes",
"futures-core",
@@ -9777,7 +9777,7 @@ checksum = "04f903f293d11f31c0c29e4148f6dc0d033a7f80cebc0282bea147611667d289"
dependencies = [
"getrandom 0.2.15",
"rand 0.8.5",
"uuid 1.10.0",
"uuid 1.11.0",
"web-time",
]
@@ -9977,7 +9977,7 @@ dependencies = [
"log",
"native-tls",
"once_cell",
"rustls 0.23.14",
"rustls 0.23.15",
"rustls-pki-types",
"serde",
"serde_json",
@@ -10038,9 +10038,9 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom 0.2.15",
"serde",
@@ -10358,7 +10358,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"axum",
@@ -10387,7 +10387,7 @@ dependencies = [
"tokio",
"tracing",
"url",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-api",
"windmill-api-client",
"windmill-common",
@@ -10399,7 +10399,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"argon2",
@@ -10426,7 +10426,7 @@ dependencies = [
"hf-hub",
"hmac",
"http 1.1.0",
"hyper 1.4.1",
"hyper 1.5.0",
"itertools 0.13.0",
"jsonwebtoken",
"lazy_static",
@@ -10470,7 +10470,7 @@ dependencies = [
"ulid",
"url",
"urlencoding",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-audit",
"windmill-common",
"windmill-git-sync",
@@ -10483,7 +10483,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"base64 0.21.7",
"chrono",
@@ -10496,12 +10496,12 @@ dependencies = [
"serde",
"serde_json",
"syn 1.0.109",
"uuid 1.10.0",
"uuid 1.11.0",
]
[[package]]
name = "windmill-audit"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"chrono",
"serde",
@@ -10514,7 +10514,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"async-stream",
@@ -10531,7 +10531,7 @@ dependencies = [
"git-version",
"hex",
"hmac",
"hyper 1.4.1",
"hyper 1.5.0",
"indexmap 2.6.0",
"itertools 0.13.0",
"lazy_static",
@@ -10554,12 +10554,13 @@ dependencies = [
"tracing-flame",
"tracing-loki",
"tracing-subscriber",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-macros",
]
[[package]]
name = "windmill-git-sync"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"regex",
"rsmq_async",
@@ -10567,19 +10568,20 @@ dependencies = [
"serde_json",
"sqlx",
"tracing",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-common",
"windmill-queue",
]
[[package]]
name = "windmill-indexer"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"bytes",
"chrono",
"futures",
"lazy_static",
"object_store",
"serde",
"serde_json",
@@ -10589,13 +10591,25 @@ dependencies = [
"tokio",
"tokio-tar",
"tracing",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-common",
]
[[package]]
name = "windmill-macros"
version = "1.405.5"
dependencies = [
"itertools 0.10.5",
"lazy_static",
"proc-macro2",
"quote",
"regex",
"syn 2.0.79",
]
[[package]]
name = "windmill-parser"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -10604,7 +10618,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -10616,7 +10630,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"gosyn",
@@ -10628,7 +10642,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -10640,7 +10654,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"itertools 0.13.0",
@@ -10651,7 +10665,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"itertools 0.13.0",
@@ -10662,7 +10676,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -10680,7 +10694,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -10697,7 +10711,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -10709,7 +10723,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"lazy_static",
@@ -10727,7 +10741,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"getrandom 0.2.15",
@@ -10748,7 +10762,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"serde_json",
@@ -10758,7 +10772,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -10784,14 +10798,14 @@ dependencies = [
"tokio",
"tracing",
"ulid",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-audit",
"windmill-common",
]
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -10801,7 +10815,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.407.1"
version = "1.409.2"
dependencies = [
"anyhow",
"async-recursion",
@@ -10856,7 +10870,7 @@ dependencies = [
"tokio-util",
"tracing",
"urlencoding",
"uuid 1.10.0",
"uuid 1.11.0",
"windmill-audit",
"windmill-common",
"windmill-git-sync",
+10 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.407.1"
version = "1.409.2"
authors.workspace = true
edition.workspace = true
@@ -14,6 +14,7 @@ members = [
"./windmill-audit",
"./windmill-git-sync",
"./windmill-indexer",
"./windmill-macros",
"./parsers/windmill-parser",
"./parsers/windmill-parser-ts",
"./parsers/windmill-parser-wasm",
@@ -23,11 +24,11 @@ members = [
"./parsers/windmill-parser-py",
"./parsers/windmill-parser-py-imports",
"./parsers/windmill-sql-datatype-parser-wasm",
"./parsers/windmill-parser-yaml",
"./parsers/windmill-parser-yaml", "windmill-macros",
]
[workspace.package]
version = "1.407.1"
version = "1.409.2"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -39,6 +40,9 @@ path = "./src/main.rs"
opt-level = 0
incremental = true
[profile.release]
lto = "thin"
[features]
default = []
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise", "windmill-indexer/enterprise"]
@@ -113,6 +117,7 @@ windmill-common = { path = "./windmill-common", default-features = false }
windmill-audit = { path = "./windmill-audit" }
windmill-git-sync = { path = "./windmill-git-sync" }
windmill-indexer = {path = "./windmill-indexer"}
windmill-macros = {path = "./windmill-macros"}
windmill-parser = { path = "./parsers/windmill-parser" }
windmill-parser-ts = { path = "./parsers/windmill-parser-ts" }
windmill-parser-py = { path = "./parsers/windmill-parser-py" }
@@ -278,6 +283,8 @@ triomphe = "^0"
tantivy = "0.22.0"
# Macro-related
proc-macro2 = "1.0"
pulldown-cmark = "0.9"
toml = "0.7"
syn = { version = "2.0.74", features = ["full"] }
+1 -1
View File
@@ -1 +1 @@
8e3fbda05392e641c15262924dc0df47cc42d036
0428068e4fbbd1380a4d8bbaab5c8e7955decdb8
+1 -1
View File
@@ -476,7 +476,7 @@ Windmill Community Edition {GIT_VERSION}
#[cfg(feature = "tantivy")]
let (index_reader, index_writer) = if should_index_jobs {
let (r, w) = windmill_indexer::indexer_ee::init_index().await?;
let (r, w) = windmill_indexer::indexer_ee::init_index(&db).await?;
(Some(r), Some(w))
} else {
(None, None)
+1 -1
View File
@@ -2745,7 +2745,7 @@ async fn test_flow_lock_all(db: Pool<Postgres>) {
"lock": null,
"path": null,
"type": "rawscript",
"content": "import * as wmill from \"https://deno.land/x/windmill@v1.50.0/mod.ts\"\n\nexport async function main() {\n return \"Hello\"\n}\n",
"content": "import * as wmill from \"https://deno.land/x/windmill@v1.50.0/mod.ts\"\n\nexport async function main() {\n return wmill\n}\n",
"language": "deno",
"input_transforms": {}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+105 -2
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.407.1
version: 1.409.2
title: Windmill API
contact:
@@ -2789,7 +2789,14 @@ paths:
oauth:
type: array
items:
type: string
type: object
properties:
type:
type: string
display_name:
type: string
required:
- type
saml:
type: string
required:
@@ -4013,6 +4020,42 @@ paths:
schema:
$ref: "#/components/schemas/Script"
/w/{workspace}/scripts/get_triggers_count/{path}:
get:
summary: get triggers count of script
operationId: getTriggersCountOfScript
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: triggers count
content:
application/json:
schema:
$ref: "#/components/schemas/TriggersCount"
/w/{workspace}/scripts/list_tokens/{path}:
get:
summary: get tokens with script scope
operationId: listTokensOfScript
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: tokens list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TruncatedToken"
/w/{workspace}/scripts/get/draft/{path}:
get:
summary: get script by path with draft
@@ -4617,6 +4660,43 @@ paths:
schema:
$ref: "#/components/schemas/Flow"
/w/{workspace}/flows/get_triggers_count/{path}:
get:
summary: get triggers count of flow
operationId: getTriggersCountOfFlow
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: triggers count
content:
application/json:
schema:
$ref: "#/components/schemas/TriggersCount"
/w/{workspace}/flows/list_tokens/{path}:
get:
summary: get tokens with flow scope
operationId: listTokensOfFlow
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: tokens list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TruncatedToken"
/w/{workspace}/flows/toggle_workspace_error_handler/{path}:
post:
summary: Toggle ON and OFF the workspace error handler for a given flow
@@ -10247,6 +10327,8 @@ components:
type: array
items:
type: string
email:
type: string
required:
- token_prefix
- created_at
@@ -10264,6 +10346,8 @@ components:
type: array
items:
type: string
workspace_id:
type: string
NewTokenImpersonate:
type: object
@@ -10275,6 +10359,8 @@ components:
format: date-time
impersonate_email:
type: string
workspace_id:
type: string
required:
- impersonate_email
@@ -11111,6 +11197,23 @@ components:
- requires_auth
- http_method
TriggersCount:
type: object
properties:
primary_schedule:
type: object
properties:
schedule:
type: string
schedule_count:
type: number
http_routes_count:
type: number
webhook_count:
type: number
email_count:
type: number
Group:
type: object
properties:
+21
View File
@@ -9,6 +9,9 @@
use std::collections::HashMap;
use crate::db::ApiAuthed;
use crate::triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
};
use crate::utils::WithStarredInfoQuery;
use crate::{
db::DB,
@@ -53,6 +56,8 @@ pub fn workspaced_service() -> Router {
.route("/update/*path", post(update_flow))
.route("/archive/*path", post(archive_flow_by_path))
.route("/delete/*path", delete(delete_flow_by_path))
.route("/get_triggers_count/*path", get(get_triggers_count))
.route("/list_tokens/*path", get(list_tokens))
.route("/get/*path", get(get_flow_by_path))
.route("/get/draft/*path", get(get_flow_by_path_w_draft))
.route("/exists/*path", get(exists_flow_by_path))
@@ -874,6 +879,22 @@ async fn update_flow(
Ok(nf.path.to_string())
}
async fn get_triggers_count(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<TriggersCount> {
let path = path.to_path();
get_triggers_count_internal(&db, &w_id, &path, true).await
}
async fn list_tokens(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
let path = path.to_path();
list_tokens_internal(&db, &w_id, &path, true).await
}
async fn get_flow_by_path(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+14
View File
@@ -275,8 +275,10 @@ pub fn require_is_owner(authed: &ApiAuthed, name: &str) -> Result<()> {
async fn update_folder(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, name)): Path<(String, String)>,
Json(mut ng): Json<UpdateFolder>,
) -> Result<String> {
@@ -367,6 +369,18 @@ async fn update_folder(
}
}
handle_deployment_metadata(
&authed.email,
&authed.username,
&db,
&w_id,
DeployedObject::Folder { path: format!("f/{}", name) },
Some(format!("Folder '{}' updated", name)),
rsmq,
true,
)
.await?;
audit_log(
&mut *tx,
&authed,
+109 -44
View File
@@ -24,7 +24,7 @@ use windmill_common::jobs::{
format_completed_job_result, format_result, CompletedJobWithFormattedResult, FormattedResult,
ENTRYPOINT_OVERRIDE,
};
use windmill_common::worker::TMP_DIR;
use windmill_common::worker::{CLOUD_HOSTED, TMP_DIR};
#[cfg(all(feature = "enterprise", feature = "parquet"))]
use windmill_common::scripts::PREVIEW_IS_CODEBASE_HASH;
@@ -81,7 +81,7 @@ use windmill_common::{METRICS_DEBUG_ENABLED, METRICS_ENABLED};
use windmill_common::{get_latest_deployed_hash_for_path, BASE_URL};
use windmill_queue::{
cancel_job, get_queued_job, get_result_by_id_from_running_flow, job_is_complete, push,
DecodeQueries, PushArgs, PushArgsOwned, PushIsolationLevel, QueueTransaction,
DecodeQueries, PushArgs, PushArgsOwned, PushIsolationLevel,
};
#[cfg(feature = "prometheus")]
@@ -544,8 +544,8 @@ pub async fn get_path_for_hash<'c>(
Ok(path)
}
pub async fn get_path_tag_limits_cache_for_hash<'c, R: rsmq_async::RsmqConnection + Send>(
tx: &mut QueueTransaction<'c, R>,
pub async fn get_path_tag_limits_cache_for_hash(
tx: &DB,
w_id: &str,
hash: i64,
) -> error::Result<(
@@ -1469,6 +1469,8 @@ async fn cancel_jobs(
}
}
uuids.extend(trivial_jobs);
Ok(Json(uuids))
}
@@ -2810,7 +2812,6 @@ pub async fn run_flow_by_path_inner(
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("run:flow/{flow_path}"))?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (tag, dedicated_worker, has_preprocessor) = sqlx::query!(
"SELECT tag, dedicated_worker, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor
@@ -2821,7 +2822,7 @@ pub async fn run_flow_by_path_inner(
flow_path,
w_id
)
.fetch_optional(&mut tx)
.fetch_optional(&db)
.await?
.map(|x| (x.tag, x.dedicated_worker, x.has_preprocessor))
.ok_or_else(|| {
@@ -2834,7 +2835,7 @@ pub async fn run_flow_by_path_inner(
check_tag_available_for_workspace(&w_id, &tag).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
tx,
@@ -2906,14 +2907,13 @@ pub async fn restart_flow(
) -> error::Result<(StatusCode, String)> {
check_license_key_valid().await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let completed_job = sqlx::query_as::<_, CompletedJob>(
"SELECT *, result->'wm_labels' as labels from completed_job WHERE id = $1 and workspace_id = $2",
)
.bind(job_id)
.bind(&w_id)
.fetch_optional(&mut tx)
.fetch_optional(&db)
.await?
.with_context(|| "Unable to find completed job with the given job UUID")?;
@@ -2931,7 +2931,7 @@ pub async fn restart_flow(
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -3007,16 +3007,16 @@ pub async fn run_script_by_path_inner(
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (job_payload, tag, _delete_after_use, timeout) =
script_path_to_payload(script_path, &mut tx, &w_id, run_query.skip_preprocessor).await?;
script_path_to_payload(script_path, &db, &w_id, run_query.skip_preprocessor).await?;
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -3049,6 +3049,11 @@ pub async fn run_script_by_path_inner(
Ok((StatusCode::CREATED, uuid.to_string()))
}
#[derive(Deserialize)]
pub struct WorkflowAsCodeQuery {
pub skip_update: Option<bool>,
}
pub async fn run_workflow_as_code(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -3056,15 +3061,38 @@ pub async fn run_workflow_as_code(
Extension(rsmq): Extension<Option<rsmq_async::MultiplexedRsmq>>,
Path((w_id, job_id, entrypoint)): Path<(String, Uuid, String)>,
Query(run_query): Query<RunJobQuery>,
Query(wkflow_query): Query<WorkflowAsCodeQuery>,
Json(task): Json<WorkflowTask>,
) -> error::Result<(StatusCode, String)> {
let mut i = 1;
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
check_tag_available_for_workspace(&w_id, &run_query.tag).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
let job = get_queued_job(&job_id, &w_id, &db).await?;
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
let job = not_found_if_none(job, "Queued Job", &job_id.to_string())?;
let (job_payload, tag, _delete_after_use, timeout) = match job.job_kind {
JobKind::Preview => (
@@ -3089,7 +3117,7 @@ pub async fn run_workflow_as_code(
JobKind::Script => {
script_path_to_payload(
job.script_path(),
&mut tx,
&db,
&w_id,
run_query.skip_preprocessor,
)
@@ -3098,6 +3126,12 @@ pub async fn run_workflow_as_code(
_ => return Err(anyhow::anyhow!("Not supported").into()),
};
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
let mut extra = HashMap::new();
extra.insert(ENTRYPOINT_OVERRIDE.to_string(), to_raw_value(&entrypoint));
@@ -3106,7 +3140,21 @@ pub async fn run_workflow_as_code(
let tag = run_query.tag.clone().or(tag).or(Some(job.tag));
let tx = PushIsolationLevel::Transaction(tx);
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
let (uuid, mut tx) = push(
&db,
@@ -3133,14 +3181,39 @@ pub async fn run_workflow_as_code(
Some(&authed.clone().into()),
)
.await?;
sqlx::query!(
"UPDATE queue SET flow_status = jsonb_set(COALESCE(flow_status, '{}'::jsonb), array[$1], jsonb_set(jsonb_set('{}'::jsonb, '{scheduled_for}', to_jsonb(now()::text)), '{name}', to_jsonb($4::text))) WHERE id = $2 AND workspace_id = $3",
uuid.to_string(),
job_id,
w_id,
entrypoint
).execute(&mut tx).await?;
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
if !wkflow_query.skip_update.unwrap_or(false) {
sqlx::query!(
"UPDATE queue SET flow_status = jsonb_set(COALESCE(flow_status, '{}'::jsonb), array[$1], jsonb_set(jsonb_set('{}'::jsonb, '{scheduled_for}', to_jsonb(now()::text)), '{name}', to_jsonb($4::text))) WHERE id = $2 AND workspace_id = $3",
uuid.to_string(),
job_id,
w_id,
entrypoint
).execute(&mut tx).await?;
} else {
tracing::info!("Skipping update of flow status for job {job_id} in workspace {w_id}");
}
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
i += 1;
}
tx.commit().await?;
if *CLOUD_HOSTED {
tracing::info!("workflow_as_code_tracing id {i} ");
}
Ok((StatusCode::CREATED, uuid.to_string()))
}
@@ -3504,15 +3577,13 @@ pub async fn run_wait_result_job_by_path_get(
let script_path = script_path.to_path();
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (job_payload, tag, delete_after_use, timeout) =
script_path_to_payload(script_path, &mut tx, &w_id, run_query.skip_preprocessor).await?;
script_path_to_payload(script_path, &db, &w_id, run_query.skip_preprocessor).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -3629,15 +3700,13 @@ pub async fn run_wait_result_script_by_path_internal(
let script_path = script_path.to_path();
check_scopes(&authed, || format!("run:script/{script_path}"))?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let (job_payload, tag, delete_after_use, timeout) =
script_path_to_payload(script_path, &mut tx, &w_id, run_query.skip_preprocessor).await?;
script_path_to_payload(script_path, &db, &w_id, run_query.skip_preprocessor).await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -3689,8 +3758,6 @@ pub async fn run_wait_result_script_by_hash(
check_queue_too_long(&db, run_query.queue_limit).await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let hash = script_hash.0;
let (
path,
@@ -3705,7 +3772,7 @@ pub async fn run_wait_result_script_by_hash(
delete_after_use,
timeout,
has_preprocessor,
) = get_path_tag_limits_cache_for_hash(&mut tx, &w_id, hash).await?;
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
if let Some(run_query_cache_ttl) = run_query.cache_ttl {
cache_ttl = Some(run_query_cache_ttl);
}
@@ -3714,7 +3781,7 @@ pub async fn run_wait_result_script_by_hash(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -3796,7 +3863,6 @@ pub async fn run_wait_result_flow_by_path_internal(
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("run:flow/{flow_path}"))?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let scheduled_for = run_query.get_scheduled_for(&db).await?;
@@ -3809,7 +3875,7 @@ pub async fn run_wait_result_flow_by_path_internal(
flow_path,
w_id
)
.fetch_optional(&mut tx)
.fetch_optional(&db)
.await?
.map(|x| (x.tag, x.dedicated_worker, x.early_return, x.has_preprocessor))
.ok_or_else(|| {
@@ -3821,7 +3887,7 @@ pub async fn run_wait_result_flow_by_path_internal(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
@@ -4127,10 +4193,10 @@ async fn run_dependencies_job(
JsonRawValue::from_string("true".to_string()).unwrap(),
);
if language == ScriptLang::Bun {
let annotation = windmill_common::worker::get_annotation_ts(&raw_code);
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(&raw_code);
hm.insert(
"npm_mode".to_string(),
JsonRawValue::from_string(annotation.npm_mode.to_string()).unwrap(),
JsonRawValue::from_string(annotation.npm.to_string()).unwrap(),
);
}
(PushArgs { extra: Some(hm), args: &ehm }, deps)
@@ -4293,7 +4359,6 @@ async fn add_batch_jobs(
}
}
"flow" => {
let mut tx = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
let mut uuids: Vec<Uuid> = Vec::new();
let payload = if let Some(ref fv) = batch_info.flow_value {
@@ -4311,6 +4376,7 @@ async fn add_batch_jobs(
))?
}
};
let mut tx = PushIsolationLevel::IsolatedRoot(db.clone(), rsmq);
for _ in 0..n {
let ehm = HashMap::new();
let (uuid, ntx) = push(
@@ -4511,7 +4577,6 @@ pub async fn run_job_by_hash_inner(
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let mut tx: QueueTransaction<'_, _> = (rsmq, user_db.begin(&authed).await?).into();
let hash = script_hash.0;
let (
@@ -4527,7 +4592,7 @@ pub async fn run_job_by_hash_inner(
_delete_after_use, // not taken into account in async endpoints
timeout,
has_preprocessor,
) = get_path_tag_limits_cache_for_hash(&mut tx, &w_id, hash).await?;
) = get_path_tag_limits_cache_for_hash(&db, &w_id, hash).await?;
check_scopes(&authed, || format!("run:script/{path}"))?;
if let Some(run_query_cache_ttl) = run_query.cache_ttl {
cache_ttl = Some(run_query_cache_ttl);
@@ -4536,7 +4601,7 @@ pub async fn run_job_by_hash_inner(
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&w_id, &tag).await?;
let tx = PushIsolationLevel::Transaction(tx);
let tx = PushIsolationLevel::Isolated(user_db, authed.clone().into(), rsmq);
let (uuid, tx) = push(
&db,
+1
View File
@@ -82,6 +82,7 @@ pub mod smtp_server_ee;
mod static_assets;
mod stripe_ee;
mod tracing_init;
mod triggers;
mod users;
mod utils;
mod variables;
+24 -3
View File
@@ -9,6 +9,9 @@
use crate::{
db::{ApiAuthed, DB},
schedule::clear_schedule,
triggers::{
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
},
users::{maybe_refresh_folders, require_owner_of_path, AuthCache},
utils::WithStarredInfoQuery,
webhook_util::{WebhookMessage, WebhookShared},
@@ -53,7 +56,7 @@ use windmill_common::{
utils::{
not_found_if_none, paginate, query_elems_from_hub, require_admin, Pagination, StripPath,
},
worker::{get_annotation_ts, to_raw_value},
worker::to_raw_value,
HUB_BASE_URL,
};
use windmill_git_sync::{handle_deployment_metadata, DeployedObject};
@@ -132,6 +135,8 @@ pub fn workspaced_service() -> Router {
.route("/archive/p/*path", post(archive_script_by_path))
.route("/get/draft/*path", get(get_script_by_path_w_draft))
.route("/get/p/*path", get(get_script_by_path))
.route("/get_triggers_count/*path", get(get_triggers_count))
.route("/list_tokens/*path", get(list_tokens))
.route("/raw/p/*path", get(raw_script_by_path))
.route("/raw_unpinned/p/*path", get(raw_script_by_path_unpinned))
.route("/exists/p/*path", get(exists_script_by_path))
@@ -601,8 +606,8 @@ async fn create_script_internal<'c>(
};
let lang = if &ns.language == &ScriptLang::Bun || &ns.language == &ScriptLang::Bunnative {
let anns = get_annotation_ts(&ns.content);
if anns.native_mode {
let anns = windmill_common::worker::TypeScriptAnnotations::parse(&ns.content);
if anns.native {
ScriptLang::Bunnative
} else {
ScriptLang::Bun
@@ -874,6 +879,22 @@ async fn get_script_by_path(
Ok(Json(script))
}
async fn list_tokens(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
let path = path.to_path();
list_tokens_internal(&db, &w_id, &path, false).await
}
async fn get_triggers_count(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<TriggersCount> {
let path = path.to_path();
get_triggers_count_internal(&db, &w_id, &path, false).await
}
async fn get_script_by_path_w_draft(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+12
View File
@@ -9,6 +9,7 @@
use ::tracing::{field, Span};
use hyper::Response;
use tower_http::trace::{MakeSpan, OnFailure, OnResponse};
use uuid::Uuid;
lazy_static::lazy_static! {
static ref LOG_REQUESTS: bool = std::env::var("LOG_REQUESTS")
@@ -45,17 +46,28 @@ impl<B> OnFailure<B> for MyOnFailure {
// tracing::error!(latency = latency.as_millis(), "response")
}
}
lazy_static::lazy_static! {
static ref TRACING_HEADER: String = std::env::var("TRACING_HEADER")
.ok().unwrap_or_else(|| "x-tracing-id".to_string());
}
#[derive(Clone)]
pub struct MyMakeSpan {}
impl<B> MakeSpan<B> for MyMakeSpan {
fn make_span(&mut self, request: &hyper::Request<B>) -> Span {
let tracing_id = request
.headers()
.get(TRACING_HEADER.as_str())
.and_then(|x| x.to_str().map(|x| x.to_string()).ok())
.unwrap_or(Uuid::new_v4().to_string());
tracing::info_span!(
"request",
method = %request.method(),
uri = %request.uri(),
username = field::Empty,
workspace_id = field::Empty,
trace_id = tracing_id,
email = field::Empty,
)
}
+131
View File
@@ -0,0 +1,131 @@
use axum::Json;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use windmill_common::error::JsonResult;
use crate::db::DB;
#[derive(Serialize, Deserialize, Debug)]
pub struct TriggerPrimarySchedule {
schedule: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TriggersCount {
primary_schedule: Option<TriggerPrimarySchedule>,
schedule_count: i64,
http_routes_count: i64,
webhook_count: i64,
email_count: i64,
}
pub(crate) async fn get_triggers_count_internal(
db: &DB,
w_id: &str,
path: &str,
is_flow: bool,
) -> JsonResult<TriggersCount> {
let primary_schedule = sqlx::query_scalar!(
"SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3",
path,
is_flow,
w_id
)
.fetch_optional(db)
.await?;
let schedule_count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
path,
is_flow,
w_id
)
.fetch_one(db)
.await?
.unwrap_or(0);
let http_routes_count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
path,
is_flow,
w_id
)
.fetch_one(db)
.await?
.unwrap_or(0);
let webhook_count = (if is_flow {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
w_id,
path,
)
} else {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]",
w_id,
path,
)
}).fetch_one(db)
.await?
.unwrap_or(0);
let email_count = (if is_flow {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
w_id,
path,
)
} else {
sqlx::query_scalar!(
"SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
w_id,
path,
)
}).fetch_one(db)
.await?
.unwrap_or(0);
Ok(Json(TriggersCount {
primary_schedule: primary_schedule.map(|s| TriggerPrimarySchedule { schedule: s }),
schedule_count,
http_routes_count,
webhook_count,
email_count,
}))
}
#[derive(FromRow, Serialize)]
pub struct TruncatedTokenWithEmail {
pub label: Option<String>,
pub token_prefix: Option<String>,
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub last_used_at: chrono::DateTime<chrono::Utc>,
pub scopes: Option<Vec<String>>,
pub email: Option<String>,
}
pub async fn list_tokens_internal(
db: &DB,
w_id: &str,
path: &str,
is_flow: bool,
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
let tokens = if is_flow {
sqlx::query_as!(
TruncatedTokenWithEmail,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
w_id, path).fetch_all(db)
.await?
} else {
sqlx::query_as!(
TruncatedTokenWithEmail,
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
w_id, path)
.fetch_all(db)
.await?
};
Ok(Json(tokens))
}
+1
View File
@@ -57,6 +57,7 @@ futures-core.workspace = true
async-stream.workspace = true
const_format.workspace = true
crc.workspace = true
windmill-macros.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl = { optional = true, workspace = true }
+12 -50
View File
@@ -13,6 +13,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use tokio::sync::RwLock;
use windmill_macros::annotations;
use crate::{error, global_settings::CUSTOM_TAGS_SETTING, server::Smtp, DB};
@@ -303,64 +304,25 @@ fn parse_file<T: FromStr>(path: &str) -> Option<T> {
.flatten()
}
#[annotations("#")]
pub struct PythonAnnotations {
pub no_cache: bool,
pub no_uv: bool,
}
#[annotations("//")]
pub struct TypeScriptAnnotations {
pub npm_mode: bool,
pub nodejs_mode: bool,
pub native_mode: bool,
pub npm: bool,
pub nodejs: bool,
pub native: bool,
pub nobundling: bool,
}
pub fn get_annotation_ts(inner_content: &str) -> TypeScriptAnnotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("//"))
.map(|x| x.to_string().replace("//", "").trim().to_string())
.collect_vec();
let nodejs_mode: bool = annotations.contains(&"nodejs".to_string());
let npm_mode: bool = annotations.contains(&"npm".to_string());
let native_mode: bool = annotations.contains(&"native".to_string());
//TODO: remove || npm_mode when bun build is more powerful
let nobundling: bool =
annotations.contains(&"nobundling".to_string()) || nodejs_mode || *DISABLE_BUNDLING;
TypeScriptAnnotations { npm_mode, nodejs_mode, native_mode, nobundling }
}
pub struct PythonAnnotations {
pub no_uv: bool,
pub no_cache: bool,
}
pub fn get_annotation_python(inner_content: &str) -> PythonAnnotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("#"))
.map(|x| x.to_string().replace("#", "").trim().to_string())
.collect_vec();
let no_uv: bool = annotations.contains(&"no_uv".to_string());
let no_cache: bool = annotations.contains(&"no_cache".to_string());
PythonAnnotations { no_uv, no_cache }
}
#[annotations("--")]
pub struct SqlAnnotations {
pub return_last_result: bool,
}
pub fn get_sql_annotations(inner_content: &str) -> SqlAnnotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("--"))
.map(|x| x.to_string().replace("--", "").trim().to_string())
.collect_vec();
let return_last_result: bool = annotations.contains(&"return_last_result".to_string());
SqlAnnotations { return_last_result }
}
pub async fn load_cache(bin_path: &str, _remote_path: &str) -> (bool, String) {
if tokio::fs::metadata(&bin_path).await.is_ok() {
(true, format!("loaded from local cache: {}\n", bin_path))
+1
View File
@@ -29,3 +29,4 @@ tempfile.workspace = true
bytes.workspace = true
object_store = { workspace = true, optional = true}
tokio-tar.workspace = true
lazy_static.workspace = true
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "windmill-macros"
version.workspace = true
authors.workspace = true
edition.workspace = true
[lib]
proc-macro = true
[dependencies]
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
# Dependencies for tests
[dev-dependencies]
# tests/annotation.rs
lazy_static.workspace = true
itertools.workspace = true
regex.workspace = true
+95
View File
@@ -0,0 +1,95 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Ident, ItemStruct, Lit};
#[proc_macro_attribute]
pub fn annotations(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemStruct);
let name = input.ident.clone();
let fields = input
.fields
.iter()
.map(|f| f.ident.clone().unwrap())
.collect::<Vec<Ident>>();
// Match on the literal to extract the string value
let comm_lit = match parse_macro_input!(attr as Lit) {
Lit::Str(lit_str) => lit_str.value(), // This will give "#" without quotes
_ => panic!("Expected a string literal"),
};
// Generate regex
let mut reg = format!("^{}|", &comm_lit);
{
for field in fields.iter() {
reg.push_str(&(field.to_string()));
reg.push_str("\\b");
}
reg.push_str(r#"|\w+"#);
}
// Example of generated regex:
// ^#
// |ann1\b|ann2\b|ann3\b|ann4\b
// |\w+
TokenStream::from(quote! {
#[derive(Default, Debug)]
#input
impl std::ops::BitOrAssign for #name{
fn bitor_assign(&mut self, rhs: Self) {
// Unfold fields
// Read more: https://docs.rs/quote/latest/quote/macro.quote.html#interpolation
#( self.#fields |= rhs.#fields; )*
}
}
impl #name {
/// Autogenerated by windmill-macros
pub fn parse(inner_content: &str) -> Self{
let mut res = Self::default();
lazy_static::lazy_static! {
static ref RE: regex::Regex = regex::Regex::new(#reg).unwrap();
}
// Create lines stream
let mut lines = inner_content.lines();
'outer: while let Some(line) = lines.next() {
// If comment sign(s) on the right place
let mut comms = false;
// New instance
// We will apply it if in line only annotations
let mut new = Self::default();
'inner: for (i, mat) in RE.find_iter(line).enumerate() {
match mat.as_str(){
#comm_lit if i == 0 => {
comms = true;
continue 'inner;
},
// Will expand into something like:
// "ann1" => new.ann1 = true,
// "ann2" => new.ann2 = true,
// "ann3" => new.ann3 = true,
#( stringify!(#fields) => new.#fields = true, )*
// Non annotations
_ => continue 'outer,
};
}
if !comms {
// We dont want to continue if line does not start with #
return res;
}
// Apply changes
res |= new;
}
res
}
}
})
}
@@ -0,0 +1,169 @@
#[cfg(test)]
mod annotations_tests {
extern crate windmill_macros;
use itertools::Itertools;
use windmill_macros::annotations;
// Previous implementation.
// We have to make sure that new one works the same as old one
fn old(inner_content: &str) -> Annotations {
let annotations = inner_content
.lines()
.take_while(|x| x.starts_with("#"))
.map(|x| x.to_string().replace("#", "").trim().to_string())
.collect_vec();
let ann1: bool = annotations.contains(&"ann1".to_string());
let ann2: bool = annotations.contains(&"ann2".to_string());
let ann3: bool = annotations.contains(&"ann3".to_string());
let ann4: bool = annotations.contains(&"ann4".to_string());
let ann5: bool = annotations.contains(&"ann5".to_string());
Annotations { ann1, ann2, ann3, ann4, ann5 }
}
#[annotations("#")]
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct Annotations {
pub ann1: bool,
pub ann2: bool,
pub ann3: bool,
pub ann4: bool,
pub ann5: bool,
}
#[annotations("//")]
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct SlashedAnnotations {
pub ann1: bool,
pub ann2: bool,
pub ann3: bool,
pub ann4: bool,
}
#[annotations("--")]
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct MinusedAnnotations {
pub ann1: bool,
pub ann2: bool,
}
// e.g. rust, TS and JS
#[test]
fn slashed_annotations() {
let cont = "// ann1
// ann2
//ann3";
assert_eq!(
SlashedAnnotations { ann1: true, ann2: true, ann3: true, ann4: false },
SlashedAnnotations::parse(cont)
);
}
// e.g. Haskell, SQL
#[test]
fn minused_annotations() {
let cont = "-- ann1
-- ann2";
assert_eq!(
MinusedAnnotations { ann1: true, ann2: true },
MinusedAnnotations::parse(cont)
);
}
#[test]
fn simple_integration() {
let cont = "# ann1";
let expected = Annotations { ann1: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
#[test]
fn multiline_integration() {
let cont = "# ann2
# ann3
# ann4
# ann5
";
let expected = Annotations {
ann1: false,
ann2: true,
ann3: true,
ann4: true,
ann5: true,
//
};
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
#[test]
fn spacing_integration() {
// First line is ignored and not used
{
let cont = "
# ann2";
let expected = Annotations { ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
// Wrong spacing for ann3
{
let cont = "# ann2
# ann3";
let expected = Annotations { ann2: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
// Drunk but valid spacing
{
let cont = "#ann1
# ann2";
let expected = Annotations { ann2: true, ann1: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
}
#[test]
fn comments_inbetween_integration() {
let cont = "# ann2
# Just comment, has nothing to do with annotations
# Another comment: ann1 ann2 ann3
# ann4 is not valid annotation
# Actual annotation next line:
# ann5
# Should be ignored
# ann3
";
let expected = Annotations { ann2: true, ann5: true, ..Default::default() };
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
#[test]
fn hash_collision() {
// TODO
}
#[test]
fn non_matching_integration() {
{
let cont = r#" "ann1", ann2 "#;
let expected = Annotations::default();
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
// Empty
{
let cont = "";
let expected = Annotations::default();
assert_eq!(expected, old(cont));
assert_eq!(expected, Annotations::parse(cont));
}
}
}
@@ -97,6 +97,13 @@ mount {
is_bind: true
}
mount {
dst: "/dev/shm"
fstype: "tmpfs"
rw: true
is_bind: false
}
mount {
src: "/dev/random"
dst: "/dev/random"
@@ -5,7 +5,6 @@ use futures::{FutureExt, TryFutureExt};
use serde_json::{json, value::RawValue, Value};
use windmill_common::error::to_anyhow;
use windmill_common::jobs::QueuedJob;
use windmill_common::worker::get_sql_annotations;
use windmill_common::{error::Error, worker::to_raw_value};
use windmill_parser_sql::{
parse_bigquery_sig, parse_db_resource, parse_sql_blocks, parse_sql_statement_named_params,
@@ -238,7 +237,7 @@ pub async fn do_bigquery(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = get_sql_annotations(query);
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let service_account = CustomServiceAccount::from_json(&database)
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
+55 -47
View File
@@ -47,7 +47,7 @@ use windmill_common::{
get_latest_hash_for_path,
jobs::{QueuedJob, PREPROCESSOR_FAKE_ENTRYPOINT},
scripts::ScriptLang,
worker::{exists_in_cache, get_annotation_ts, save_cache, write_file},
worker::{exists_in_cache, save_cache, write_file},
DB,
};
@@ -663,7 +663,7 @@ pub async fn prebundle_bun_script(
if exists_in_cache(&local_path, &remote_path).await {
return Ok(());
}
let annotation = get_annotation_ts(inner_content);
let annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
if annotation.nobundling {
return Ok(());
}
@@ -676,9 +676,9 @@ pub async fn prebundle_bun_script(
&token,
w_id,
script_path,
if annotation.nodejs_mode {
if annotation.nodejs {
LoaderMode::NodeBundle
} else if annotation.native_mode {
} else if annotation.native {
LoaderMode::BrowserBundle
} else {
LoaderMode::BunBundle
@@ -800,7 +800,7 @@ pub async fn handle_bun_job(
new_args: &mut Option<HashMap<String, Box<RawValue>>>,
occupancy_metrics: &mut OccupancyMetrics,
) -> error::Result<Box<RawValue>> {
let mut annotation = windmill_common::worker::get_annotation_ts(inner_content);
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() {
@@ -822,7 +822,7 @@ pub async fn handle_bun_job(
if !codebase.is_some() && !has_bundle_cache {
let _ = write_file(job_dir, "main.ts", inner_content)?;
} else if !annotation.native_mode && codebase.is_none() {
} else if !annotation.native && codebase.is_none() {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#)?;
};
@@ -830,7 +830,7 @@ pub async fn handle_bun_job(
get_common_bun_proc_envs(Some(&base_internal_url)).await;
if codebase.is_some() {
annotation.nodejs_mode = true
annotation.nodejs = true
}
let (main_override, apply_preprocessor) = match get_main_override(job.args.as_ref()) {
Some(main_override) => {
@@ -844,7 +844,7 @@ pub async fn handle_bun_job(
};
#[cfg(not(feature = "enterprise"))]
if annotation.nodejs_mode || annotation.npm_mode {
if annotation.nodejs || annotation.npm {
return Err(error::Error::ExecutionErr(
"Nodejs / npm mode is an EE feature".to_string(),
));
@@ -875,20 +875,20 @@ pub async fn handle_bun_job(
pull_codebase(&job.workspace_id, codebase, job_dir).await?;
} else if let Some(reqs) = requirements_o.as_ref() {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
if splitted.len() != 2 && !annotation.npm_mode {
if splitted.len() != 2 && !annotation.npm {
return Err(error::Error::ExecutionErr(
format!("Invalid requirements, expected to find //bun.lockb split pattern in reqs. Found: |{reqs}|")
));
}
let _ = write_file(job_dir, "package.json", &splitted[0])?;
let lockb = if annotation.npm_mode { "" } else { splitted[1] };
let lockb = if annotation.npm { "" } else { splitted[1] };
if lockb != EMPTY_FILE {
let mut skip_install = false;
let mut create_buntar = false;
let mut buntar_path = "".to_string();
if !annotation.npm_mode {
if !annotation.npm {
let _ = write_lockb(&splitted[1], job_dir).await?;
let mut sha_path = sha2::Sha256::new();
@@ -921,7 +921,7 @@ pub async fn handle_bun_job(
job_dir,
worker_name,
common_bun_proc_envs.clone(),
annotation.npm_mode,
annotation.npm,
&mut Some(occupancy_metrics),
)
.await?;
@@ -963,7 +963,7 @@ pub async fn handle_bun_job(
worker_name,
false,
None,
annotation.npm_mode,
annotation.npm,
&mut Some(occupancy_metrics),
)
.await?;
@@ -971,19 +971,19 @@ pub async fn handle_bun_job(
// }
}
let mut init_logs = if annotation.native_mode {
let mut init_logs = if annotation.native {
"\n\n--- NATIVE CODE EXECUTION ---\n".to_string()
} else if has_bundle_cache {
if annotation.nodejs_mode {
if annotation.nodejs {
"\n\n--- NODE BUNDLE SNAPSHOT EXECUTION ---\n".to_string()
} else {
"\n\n--- BUN BUNDLE SNAPSHOT EXECUTION ---\n".to_string()
}
} else if codebase.is_some() {
"\n\n--- NODE CODEBASE SNAPSHOT EXECUTION ---\n".to_string()
} else if annotation.native_mode {
} else if annotation.native {
"\n\n--- NATIVE CODE EXECUTION ---\n".to_string()
} else if annotation.nodejs_mode {
} else if annotation.nodejs {
write_file(job_dir, "main.ts", &remove_pinned_imports(inner_content)?)?;
"\n\n--- NODE CODE EXECUTION ---\n".to_string()
} else {
@@ -1003,7 +1003,7 @@ pub async fn handle_bun_job(
}
let write_wrapper_f = async {
if !has_bundle_cache && annotation.native_mode {
if !has_bundle_cache && annotation.native {
return Ok(()) as error::Result<()>;
}
// let mut start = Instant::now();
@@ -1105,6 +1105,16 @@ try {{
if (step_id) {{
err["step_id"] = step_id;
}}
const extra = {{}};
Object.getOwnPropertyNames(e).forEach((key) => {{
if (['line', 'name', 'stack', 'column', 'message', 'sourceURL', 'originalLine', 'originalColumn'].includes(key)) {{
return;
}}
extra[key] = e[key];
}});
if (Object.keys(extra).length > 0) {{
err["extra"] = extra;
}}
await fs.writeFile("result.json", JSON.stringify(err));
process.exit(1);
}}
@@ -1116,7 +1126,7 @@ try {{
let reserved_variables_args_out_f = async {
let args_and_out_f = async {
if !annotation.native_mode {
if !annotation.native {
create_args_and_out_file(&client, job, job_dir, db).await?;
}
Ok(()) as Result<()>
@@ -1133,7 +1143,7 @@ try {{
let build_cache = !has_bundle_cache
&& !annotation.nobundling
&& !codebase.is_some()
&& (requirements_o.is_some() || annotation.native_mode);
&& (requirements_o.is_some() || annotation.native);
let write_loader_f = async {
if build_cache {
@@ -1143,9 +1153,9 @@ try {{
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
if annotation.nodejs_mode {
if annotation.nodejs {
LoaderMode::NodeBundle
} else if annotation.native_mode {
} else if annotation.native {
LoaderMode::BrowserBundle
} else {
LoaderMode::BunBundle
@@ -1161,7 +1171,7 @@ try {{
&client.get_token().await,
&job.workspace_id,
&job.script_path(),
if annotation.nodejs_mode {
if annotation.nodejs {
LoaderMode::Node
} else {
LoaderMode::Bun
@@ -1207,7 +1217,7 @@ try {{
}
}
}
if !annotation.native_mode {
if !annotation.native {
let ex_wrapper = read_file_content(&format!("{job_dir}/wrapper.mjs")).await?;
write_file(
job_dir,
@@ -1221,7 +1231,7 @@ try {{
}
fs::remove_file(format!("{job_dir}/main.ts"))?;
has_bundle_cache = true;
} else if annotation.nodejs_mode {
} else if annotation.nodejs {
generate_wrapper_mjs(
job_dir,
&job.workspace_id,
@@ -1237,9 +1247,14 @@ try {{
.await?;
}
}
if annotation.native_mode {
if annotation.native {
#[cfg(not(feature = "deno_core"))]
return Ok(to_raw_value("").unwrap());
{
tracing::error!(
r#""deno_core" feature is not activated, but "//native" annotation used. Returning empty value..."#
);
return Ok(to_raw_value("").unwrap());
}
#[cfg(feature = "deno_core")]
{
@@ -1299,14 +1314,7 @@ try {{
job_dir,
"run.config.proto",
&NSJAIL_CONFIG_RUN_BUN_CONTENT
.replace(
"{LANG}",
if annotation.nodejs_mode {
"nodejs"
} else {
"bun"
},
)
.replace("{LANG}", if annotation.nodejs { "nodejs" } else { "bun" })
.replace("{JOB_DIR}", job_dir)
.replace("{CACHE_DIR}", BUN_CACHE_DIR)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
@@ -1314,7 +1322,7 @@ try {{
"{SHARED_MOUNT}",
&shared_mount.replace(
"/tmp/shared",
if annotation.nodejs_mode {
if annotation.nodejs {
"/tmp/nodejs/shared"
} else {
"/tmp/bun/shared"
@@ -1324,7 +1332,7 @@ try {{
)?;
let mut nsjail_cmd = Command::new(NSJAIL_PATH.as_str());
let args = if annotation.nodejs_mode {
let args = if annotation.nodejs {
vec![
"--config",
"run.config.proto",
@@ -1368,7 +1376,7 @@ try {{
.stderr(Stdio::piped());
start_child_process(nsjail_cmd, NSJAIL_PATH.as_str()).await?
} else {
let cmd = if annotation.nodejs_mode {
let cmd = if annotation.nodejs {
let script_path = format!("{job_dir}/wrapper.mjs");
let mut bun_cmd = Command::new(&*NODE_BIN_PATH);
@@ -1420,7 +1428,7 @@ try {{
start_child_process(
cmd,
if annotation.nodejs_mode {
if annotation.nodejs {
&*NODE_BIN_PATH
} else {
&*BUN_PATH
@@ -1435,7 +1443,7 @@ try {{
mem_peak,
canceled_by,
child,
false,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
"bun run",
@@ -1525,10 +1533,10 @@ pub async fn start_worker(
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(Some(&base_internal_url)).await;
let mut annotation = windmill_common::worker::get_annotation_ts(inner_content);
let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
//TODO: remove this when bun dedicated workers work without issues
annotation.nodejs_mode = true;
annotation.nodejs = true;
let context = variables::get_reserved_variables(
db,
@@ -1582,7 +1590,7 @@ pub async fn start_worker(
job_dir,
worker_name,
common_bun_proc_envs.clone(),
annotation.npm_mode,
annotation.npm,
&mut None,
)
.await?;
@@ -1603,7 +1611,7 @@ pub async fn start_worker(
worker_name,
false,
None,
annotation.npm_mode,
annotation.npm,
&mut None,
)
.await?;
@@ -1681,7 +1689,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
token,
w_id,
script_path,
if annotation.nodejs_mode {
if annotation.nodejs {
LoaderMode::Node
} else {
LoaderMode::Bun
@@ -1690,7 +1698,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
.await?;
}
if annotation.nodejs_mode && !codebase.is_some() {
if annotation.nodejs && !codebase.is_some() {
generate_wrapper_mjs(
job_dir,
w_id,
@@ -1706,7 +1714,7 @@ for await (const line of Readline.createInterface({{ input: process.stdin }})) {
.await?;
}
if annotation.nodejs_mode {
if annotation.nodejs {
let script_path = format!("{job_dir}/wrapper.mjs");
handle_dedicated_process(
@@ -126,6 +126,7 @@ pub async fn generate_deno_lock(
"--unstable-http",
"--lock=lock.json",
"--frozen=false",
"--allow-import",
"--import-map",
&import_map_path,
"main.ts",
@@ -372,6 +373,7 @@ try {{
args.push(allow_read.as_str());
args.push("--allow-write=./");
args.push("--allow-env");
args.push("--allow-import");
args.push("--allow-run=git,/usr/bin/chromium");
} else {
args.push("-A");
+24 -3
View File
@@ -414,9 +414,30 @@ async fn get_mem_peak(pid: Option<u32>, nsjail: bool) -> i32 {
return -1;
}
let pid = if nsjail {
// This is a bit hacky, but the process id of the nsjail process is the pid of nsjail + 1.
// Ideally, we would get the number from fork() itself. This works in MOST cases.
pid.unwrap() + 1
// Read /proc/<nsjail_pid>/task/<nsjail_pid>/children and extract pid
let nsjail_pid = pid.unwrap();
let children_path = format!("/proc/{}/task/{}/children", nsjail_pid, nsjail_pid);
if let Ok(mut file) = File::open(children_path).await {
let mut contents = String::new();
if tokio::io::AsyncReadExt::read_to_string(&mut file, &mut contents)
.await
.is_ok()
{
if let Some(child_pid) = contents.split_whitespace().next() {
if let Ok(child_pid) = child_pid.parse::<u32>() {
child_pid
} else {
return -1;
}
} else {
return -1;
}
} else {
return -1;
}
} else {
return -1;
}
} else {
pid.unwrap()
};
+1 -1
View File
@@ -247,7 +247,7 @@ pub async fn eval_timeout(
#[cfg(not(feature = "deno_core"))]
{
#[allow(unreachable_code)]
return todo!();
return Err(anyhow::anyhow!("Deno core is not enabled".to_string()).into());
}
#[cfg(feature = "deno_core")]
@@ -9,7 +9,7 @@ use tokio::net::TcpStream;
use tokio_util::compat::TokioAsyncWriteCompatExt;
use uuid::Uuid;
use windmill_common::error::{self, Error};
use windmill_common::worker::{get_sql_annotations, to_raw_value};
use windmill_common::worker::to_raw_value;
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser_sql::{parse_db_resource, parse_mssql_sig};
use windmill_queue::{append_logs, CanceledBy};
@@ -68,7 +68,7 @@ pub async fn do_mssql(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = get_sql_annotations(query);
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let mut config = Config::new();
@@ -13,7 +13,7 @@ use tokio::sync::Mutex;
use windmill_common::{
error::{to_anyhow, Error},
jobs::QueuedJob,
worker::{get_sql_annotations, to_raw_value},
worker::to_raw_value,
};
use windmill_parser_sql::{
parse_db_resource, parse_mysql_sig, parse_sql_blocks, parse_sql_statement_named_params,
@@ -148,7 +148,7 @@ pub async fn do_mysql(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = get_sql_annotations(query);
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let opts = OptsBuilder::default()
.db_name(Some(database.database))
+2 -2
View File
@@ -30,7 +30,7 @@ use tokio_postgres::{
};
use uuid::Uuid;
use windmill_common::error::{self, Error};
use windmill_common::worker::{get_sql_annotations, to_raw_value, CLOUD_HOSTED};
use windmill_common::worker::{to_raw_value, CLOUD_HOSTED};
use windmill_common::{error::to_anyhow, jobs::QueuedJob};
use windmill_parser::{Arg, Typ};
use windmill_parser_sql::{
@@ -191,7 +191,7 @@ pub async fn do_postgresql(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = get_sql_annotations(query);
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let sslmode = match database.sslmode.as_deref() {
Some("allow") => "prefer".to_string(),
+1 -1
View File
@@ -345,7 +345,7 @@ try {{
mem_peak,
canceled_by,
child,
false,
!*DISABLE_NSJAIL,
worker_name,
&job.workspace_id,
"php run",
@@ -498,7 +498,10 @@ except BaseException as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
tb = traceback.format_tb(exc_traceback)
with open(result_json, 'w') as f:
err = {{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}
err = {{ "message": str(e), "name": e.__class__.__name__, "stack": '\n'.join(tb[1:]) }}
extra = e.__dict__
if extra and len(extra) > 0:
err['extra'] = extra
flow_node_id = os.environ.get('WM_FLOW_STEP_ID')
if flow_node_id:
err['step_id'] = flow_node_id
@@ -895,7 +898,7 @@ async fn handle_python_deps(
let requirements = match requirements_o {
Some(r) => r,
None => {
let annotation = windmill_common::worker::get_annotation_python(inner_content);
let annotation = windmill_common::worker::PythonAnnotations::parse(inner_content);
let mut already_visited = vec![];
let requirements = windmill_parser_py_imports::parse_python_imports(
@@ -9,7 +9,6 @@ use serde_json::{json, value::RawValue, Value};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use windmill_common::error::to_anyhow;
use windmill_common::worker::get_sql_annotations;
use windmill_common::jobs::QueuedJob;
use windmill_common::{error::Error, worker::to_raw_value};
@@ -266,7 +265,7 @@ pub async fn do_snowflake(
return Err(Error::BadRequest("Missing database argument".to_string()));
};
let annotations = get_sql_annotations(query);
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
let qualified_username = format!(
"{}.{}",
+14 -12
View File
@@ -12,7 +12,7 @@ use windmill_common::flows::{FlowModule, FlowModuleValue};
use windmill_common::get_latest_deployed_hash_for_path;
use windmill_common::jobs::JobPayload;
use windmill_common::scripts::ScriptHash;
use windmill_common::worker::{get_annotation_ts, to_raw_value, to_raw_value_owned, write_file};
use windmill_common::worker::{to_raw_value, to_raw_value_owned, write_file};
use windmill_common::{
error::{self, to_anyhow},
flows::FlowValue,
@@ -953,10 +953,10 @@ async fn lock_modules<'c>(
}
if language == ScriptLang::Bun || language == ScriptLang::Bunnative {
let anns = get_annotation_ts(&content);
if anns.native_mode && language == ScriptLang::Bun {
let anns = windmill_common::worker::TypeScriptAnnotations::parse(&content);
if anns.native && language == ScriptLang::Bun {
language = ScriptLang::Bunnative;
} else if !anns.native_mode && language == ScriptLang::Bunnative {
} else if !anns.native && language == ScriptLang::Bunnative {
language = ScriptLang::Bun;
};
}
@@ -1003,10 +1003,10 @@ async fn lock_modules<'c>(
fn skip_creating_new_lock(language: &ScriptLang, content: &str) -> bool {
if language == &ScriptLang::Bun || language == &ScriptLang::Bunnative {
let anns = get_annotation_ts(&content);
if anns.native_mode && language == &ScriptLang::Bun {
let anns = windmill_common::worker::TypeScriptAnnotations::parse(&content);
if anns.native && language == &ScriptLang::Bun {
return false;
} else if !anns.native_mode && language == &ScriptLang::Bunnative {
} else if !anns.native && language == &ScriptLang::Bunnative {
return false;
};
}
@@ -1077,11 +1077,13 @@ async fn lock_modules_app(
match new_lock {
Ok(new_lock) => {
append_logs(&job.id, &job.workspace_id, logs, db).await;
let anns = get_annotation_ts(&content);
let nlang = if anns.native_mode && language == ScriptLang::Bun {
let anns =
windmill_common::worker::TypeScriptAnnotations::parse(
&content,
);
let nlang = if anns.native && language == ScriptLang::Bun {
Some(ScriptLang::Bunnative)
} else if !anns.native_mode && language == ScriptLang::Bunnative
{
} else if !anns.native && language == ScriptLang::Bunnative {
Some(ScriptLang::Bun)
} else {
None
@@ -1437,7 +1439,7 @@ async fn capture_dependency_job(
}
ScriptLang::Bun | ScriptLang::Bunnative => {
let npm_mode = npm_mode.unwrap_or_else(|| {
windmill_common::worker::get_annotation_ts(job_raw_code).npm_mode
windmill_common::worker::TypeScriptAnnotations::parse(job_raw_code).npm
});
if !raw_deps {
let _ = write_file(job_dir, "main.ts", job_raw_code)?;
+1 -1
View File
@@ -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.407.1";
export const VERSION = "v1.409.2";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+4 -4
View File
@@ -1,6 +1,6 @@
// deno-lint-ignore-file no-explicit-any
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
import { colors, Command, log, SEP, Table, yamlParse } from "./deps.ts";
import { colors, Command, log, SEP, Table, yamlParseFile } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { ListableApp, Policy } from "./gen/types.gen.ts";
@@ -25,7 +25,7 @@ export async function pushApp(
return;
}
alreadySynced.push(localPath);
remotePath.replaceAll(SEP, "/");
remotePath = remotePath.replaceAll(SEP, "/");
let app: any = undefined;
// deleting old app if it exists in raw mode
try {
@@ -40,8 +40,8 @@ export async function pushApp(
if (!localPath.endsWith(SEP)) {
localPath += SEP;
}
const localAppRaw = await Deno.readTextFile(localPath + "app.yaml");
const localApp = yamlParse(localAppRaw) as AppFile;
const path = localPath + "app.yaml";
const localApp = (await yamlParseFile(path)) as AppFile;
function replaceInlineScripts(rec: any) {
if (!rec) {
+2 -4
View File
@@ -1,4 +1,4 @@
import { log, yamlParse } from "./deps.ts";
import { log, yamlParseFile } from "./deps.ts";
export interface SyncOptions {
stateful?: boolean;
@@ -40,9 +40,7 @@ export interface Codebase {
export async function readConfigFile(): Promise<SyncOptions> {
try {
const conf = yamlParse(
await Deno.readTextFile("wmill.yaml")
) as SyncOptions;
const conf = (await yamlParseFile("wmill.yaml")) as SyncOptions;
if (conf?.defaultTs == undefined) {
log.warn(
"No defaultTs defined in your wmill.yaml. Using 'bun' as default."
+23 -1
View File
@@ -21,7 +21,29 @@ export { copy } from "jsr:@std/io/copy";
export { readAll } from "jsr:@std/io/read-all";
export * as log from "jsr:@std/log";
export { stringify as yamlStringify, parse as yamlParse } from "jsr:@std/yaml";
export { stringify as yamlStringify } from "jsr:@std/yaml";
import { parse as yamlParse, ParseOptions } from "jsr:@std/yaml";
export async function yamlParseFile(path: string, options: ParseOptions = {}) {
try {
return yamlParse(await Deno.readTextFile(path), options);
} catch (e) {
throw new Error(`Error parsing yaml ${path}`, { cause: e });
}
}
export function yamlParseContent(
path: string,
content: string,
options: ParseOptions = {}
) {
try {
return yamlParse(content, options);
} catch (e) {
throw new Error(`Error parsing yaml ${path}`, { cause: e });
}
}
// other
+20 -18
View File
@@ -1,7 +1,7 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions, isSuperset } from "./types.ts";
import { Confirm, SEP, log, yamlStringify } from "./deps.ts";
import { colors, Command, Table, yamlParse } from "./deps.ts";
import { colors, Command, Table, yamlParseFile } from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
@@ -29,21 +29,23 @@ export function replaceInlineScripts(
) {
modules.forEach((m) => {
if (m.value.type == "rawscript") {
const path = m.value.content.split(" ")[1];
m.value.content = Deno.readTextFileSync(localPath + path);
const lock = m.value.lock;
if (removeLocks && removeLocks.includes(path)) {
m.value.lock = undefined;
} else if (
lock &&
typeof lock == "string" &&
lock.trimStart().startsWith("!inline ")
) {
const path = lock.split(" ")[1];
try {
m.value.lock = readInlinePathSync(localPath + path);
} catch {
log.error(`Lock file ${path} not found`);
if (m.value.content.startsWith("!inline")) {
const path = m.value.content.split(" ")[1];
m.value.content = Deno.readTextFileSync(localPath + path);
const lock = m.value.lock;
if (removeLocks && removeLocks.includes(path)) {
m.value.lock = undefined;
} else if (
lock &&
typeof lock == "string" &&
lock.trimStart().startsWith("!inline ")
) {
const path = lock.split(" ")[1];
try {
m.value.lock = readInlinePathSync(localPath + path);
} catch {
log.error(`Lock file ${path} not found`);
}
}
}
} else if (m.value.type == "forloopflow") {
@@ -87,8 +89,7 @@ export async function pushFlow(
if (!localPath.endsWith(SEP)) {
localPath += SEP;
}
const localFlowRaw = await Deno.readTextFile(localPath + "flow.yaml");
const localFlow = yamlParse(localFlowRaw) as FlowFile;
const localFlow = (await yamlParseFile(localPath + "flow.yaml")) as FlowFile;
replaceInlineScripts(localFlow.value.modules, localPath, undefined);
@@ -120,6 +121,7 @@ export async function pushFlow(
});
} catch (e) {
throw new Error(
//@ts-ignore
`Failed to create flow ${remotePath}: ${e.body ?? e.message}`
);
}
+2
View File
@@ -73,6 +73,7 @@ export async function pushFolder(
},
});
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -87,6 +88,7 @@ export async function pushFolder(
},
});
} catch (e) {
//@ts-ignore
throw Error(`Failed to create folder ${name}: ${e.body ?? e.message}`);
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ export const OpenAPI: OpenAPIConfig = {
PASSWORD: undefined,
TOKEN: getEnv("WM_TOKEN"),
USERNAME: undefined,
VERSION: '1.401.0',
VERSION: '1.407.2',
WITH_CREDENTIALS: true,
interceptors: {
request: new Interceptors(),
+4
View File
@@ -1015,6 +1015,9 @@ export type FlowModule = {
skip_if_stopped?: boolean;
expr: string;
};
skip_if?: {
expr: string;
};
sleep?: InputTransform;
cache_ttl?: number;
timeout?: number;
@@ -1176,6 +1179,7 @@ export type FlowStatusModule = {
approver: string;
}>;
failed_retries?: Array<(string)>;
skipped?: boolean;
};
export type type4 = 'WaitingForPriorSteps' | 'WaitingForEvents' | 'WaitingForExecutor' | 'InProgress' | 'Success' | 'Failure';
+6 -5
View File
@@ -3,7 +3,7 @@ import {
path,
Confirm,
yamlStringify,
yamlParse,
yamlParseFile,
Command,
setClient,
Table,
@@ -459,9 +459,9 @@ async function instancePush(opts: GlobalOptions & InstanceSyncOptions) {
}
try {
const workspaceSettings = yamlParse(
await Deno.readTextFile("settings.yaml")
) as SimplifiedSettings;
const workspaceSettings = (await yamlParseFile(
"settings.yaml"
)) as SimplifiedSettings;
await workspaceSetup(
{
token: instance.token,
@@ -578,6 +578,7 @@ async function whoami(opts: {}) {
log.info(JSON.stringify(whoamiInfo, null, 2));
} catch (error) {
log.error(
//@ts-ignore
colors.red(`Failed to retrieve whoami information: ${error.message}`)
);
}
@@ -620,7 +621,7 @@ const command = new Command()
.description("Remove an instance")
.complete("instance", async () => (await allInstances()).map((x) => x.name))
.arguments("<instance:string:instance>")
.action(async (instance) => {
.action(async (instance: any) => {
const instances = await allInstances();
const choice = (await Select.prompt({
+1 -1
View File
@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.407.1";
export const VERSION = "1.409.2";
const command = new Command()
.name("wmill")
+16 -10
View File
@@ -1,6 +1,13 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "./types.ts";
import { SEP, colors, log, path, yamlParse, yamlStringify } from "./deps.ts";
import {
SEP,
colors,
log,
path,
yamlParseFile,
yamlStringify,
} from "./deps.ts";
import {
ScriptMetadata,
defaultScriptMetadata,
@@ -119,9 +126,9 @@ export async function generateFlowLockInternal(
return remote_path;
}
const flowValue = yamlParse(
await Deno.readTextFile(folder! + SEP + "flow.yaml")
) as FlowFile;
const flowValue = (await yamlParseFile(
folder! + SEP + "flow.yaml"
)) as FlowFile;
if (!justUpdateMetadataLock) {
const changedScripts = [];
@@ -798,7 +805,7 @@ export async function parseMetadataFile(
try {
metadataFilePath = scriptPath + ".script.yaml";
await Deno.stat(metadataFilePath);
const payload: any = yamlParse(await Deno.readTextFile(metadataFilePath));
const payload: any = await yamlParseFile(metadataFilePath);
replaceLock(payload);
return {
@@ -840,9 +847,9 @@ export async function parseMetadataFile(
codebases,
false
);
scriptInitialMetadata = yamlParse(
await Deno.readTextFile(metadataFilePath)
) as ScriptMetadata;
scriptInitialMetadata = (await yamlParseFile(
metadataFilePath
)) as ScriptMetadata;
replaceLock(scriptInitialMetadata);
} catch (e) {
log.info(
@@ -868,8 +875,7 @@ interface Lock {
const WMILL_LOCKFILE = "wmill-lock.yaml";
export async function readLockfile(): Promise<Lock> {
try {
const lockfile = await Deno.readTextFile(WMILL_LOCKFILE);
const read = yamlParse(lockfile);
const read = await yamlParseFile(WMILL_LOCKFILE);
if (typeof read == "object") {
return read as Lock;
} else {
+32 -21
View File
@@ -1,7 +1,7 @@
import { yamlStringify } from "./deps.ts";
import { Confirm } from "./deps.ts";
import { colors } from "./deps.ts";
import { yamlParse } from "./deps.ts";
import { yamlParseFile } from "./deps.ts";
import { log } from "./deps.ts";
import { compareInstanceObjects } from "./instance.ts";
import { isSuperset } from "./types.ts";
@@ -259,17 +259,24 @@ export async function pushWorkspaceKey(
}
}
export async function readInstanceSettings() {
let localSettings: GlobalSetting[] = [];
try {
localSettings = (await yamlParseFile(
"instance_settings.yaml"
)) as GlobalSetting[];
} catch {
log.warn("No instance_settings.yaml found");
}
return localSettings;
}
export async function pullInstanceSettings(preview = false) {
const remoteSettings = await wmill.listGlobalSettings();
if (preview) {
let localSettings: GlobalSetting[] = [];
try {
localSettings = yamlParse(
await Deno.readTextFile("instance_settings.yaml")
) as GlobalSetting[];
} catch {}
const localSettings: GlobalSetting[] = await readInstanceSettings();
return compareInstanceObjects(
remoteSettings,
@@ -294,9 +301,7 @@ export async function pushInstanceSettings(
baseUrl?: string
) {
const remoteSettings = await wmill.listGlobalSettings();
let localSettings = (await Deno.readTextFile("instance_settings.yaml")
.then((raw) => yamlParse(raw))
.catch(() => [])) as GlobalSetting[];
let localSettings: GlobalSetting[] = await readInstanceSettings();
if (baseUrl) {
localSettings = localSettings.filter((s) => s.name !== "base_url");
@@ -354,6 +359,17 @@ export async function pushInstanceSettings(
}
}
export async function readLocalConfigs() {
let localConfigs: Config[] = [];
try {
localConfigs = (await yamlParseFile("instance_configs.yaml")) as Config[];
} catch {
log.warn("No instance_configs.yaml found");
}
return localConfigs;
}
export async function pullInstanceConfigs(preview = false) {
const remoteConfigs = (await wmill.listConfigs()).map((x) => {
return {
@@ -363,12 +379,7 @@ export async function pullInstanceConfigs(preview = false) {
});
if (preview) {
let localConfigs: Config[] = [];
try {
localConfigs = yamlParse(
await Deno.readTextFile("instance_configs.yaml")
) as Config[];
} catch {}
const localConfigs: Config[] = await readLocalConfigs();
return compareInstanceObjects(
remoteConfigs,
@@ -395,9 +406,7 @@ export async function pushInstanceConfigs(preview: boolean = false) {
name: removeWorkerPrefix(x.name),
};
});
const localConfigs = (await Deno.readTextFile("instance_configs.yaml")
.then((raw) => yamlParse(raw))
.catch(() => [])) as Config[];
const localConfigs = await readLocalConfigs();
if (preview) {
return compareInstanceObjects(
@@ -415,7 +424,9 @@ export async function pushInstanceConfigs(preview: boolean = false) {
}
try {
await wmill.updateConfig({
name: config.name.startsWith('worker__') ? config.name : `worker__${config.name}`,
name: config.name.startsWith("worker__")
? config.name
: `worker__${config.name}`,
requestBody: config.config,
});
} catch (err) {
+6 -6
View File
@@ -9,7 +9,7 @@ import {
path,
log,
yamlStringify,
yamlParse,
yamlParseContent,
SEP,
} from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
@@ -112,7 +112,7 @@ async function addCodebaseDigestIfRelevant(
if (isTs) {
const c = findCodebase(path, codebases);
if (c) {
const parsed: any = yamlParse(content);
const parsed: any = yamlParseContent(path, content);
if (parsed && typeof parsed == "object") {
parsed["codebase"] = c.digest;
parsed["lock"] = undefined;
@@ -660,7 +660,7 @@ export async function elementsToMap(
if (json) {
o = JSON.parse(content);
} else {
o = yamlParse(content);
o = yamlParseContent(path, content);
}
if (o["is_secret"]) {
continue;
@@ -704,7 +704,7 @@ async function compareDynFSElement(
function parseYaml(k: string, v: string) {
if (k.endsWith(".script.yaml")) {
const o: any = yamlParse(v);
const o: any = yamlParseContent(k, v);
if (typeof o == "object") {
if (Array.isArray(o?.["lock"])) {
o["lock"] = o["lock"].join("\n");
@@ -715,7 +715,7 @@ async function compareDynFSElement(
}
return o;
} else if (k.endsWith(".app.yaml")) {
const o: any = yamlParse(v);
const o: any = yamlParseContent(k, v);
const o2 = o["policy"];
if (typeof o2 == "object") {
@@ -728,7 +728,7 @@ async function compareDynFSElement(
}
return o;
} else {
return yamlParse(v);
return yamlParseContent(k, v);
}
}
for (const [k, v] of Object.entries(m1)) {
+5 -5
View File
@@ -6,7 +6,7 @@ import {
colors,
log,
path,
yamlParse,
yamlParseContent,
yamlStringify,
} from "./deps.ts";
import { pushApp } from "./apps.ts";
@@ -115,7 +115,7 @@ export async function pushObj(
newObj: any,
plainSecrets: boolean,
alreadySynced: string[],
message?: string,
message?: string
) {
const typeEnding = getTypeStrFromPath(p);
@@ -155,7 +155,7 @@ export async function pushObj(
export function parseFromPath(p: string, content: string): any {
return p.endsWith(".yaml")
? yamlParse(content)
? yamlParseContent(p, content)
: p.endsWith(".json")
? JSON.parse(content)
: content;
@@ -164,7 +164,7 @@ export function parseFromFile(p: string): any {
if (p.endsWith(".json")) {
return JSON.parse(Deno.readTextFileSync(p));
} else if (p.endsWith(".yaml") || p.endsWith(".yml")) {
return yamlParse(Deno.readTextFileSync(p));
return yamlParseContent(p, Deno.readTextFileSync(p));
} else {
throw new Error("Could not read file " + p);
}
@@ -227,7 +227,7 @@ export function getTypeStrFromPath(
return typeEnding;
} else {
if (isFileResource(p)) {
return "resource"
return "resource";
}
throw new Error("Could not infer type of path " + JSON.stringify(parsed));
}
+34 -17
View File
@@ -13,7 +13,7 @@ import {
log,
Table,
yamlStringify,
yamlParse,
yamlParseFile,
} from "./deps.ts";
import * as wmill from "./gen/services.gen.ts";
import {
@@ -170,6 +170,7 @@ export async function pushWorkspaceUser(
},
});
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -189,6 +190,7 @@ export async function pushWorkspaceUser(
},
});
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -251,6 +253,7 @@ export async function pushGroup(
},
});
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -301,6 +304,7 @@ export async function pushGroup(
});
}
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -329,6 +333,7 @@ export async function pushGroup(
},
});
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -368,11 +373,13 @@ export async function pushGroup(
});
}
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
}
} catch (e) {
//@ts-ignore
console.error(e.body);
throw e;
}
@@ -383,11 +390,7 @@ export async function pullInstanceUsers(preview: boolean = false) {
const remoteUsers = await wmill.globalUsersExport();
if (preview) {
let localUsers: ExportedUser[] = [];
try {
const raw = await Deno.readTextFile("instance_users.yaml");
localUsers = yamlParse(raw) as ExportedUser[];
} catch {}
const localUsers: ExportedUser[] = await readInstanceUsers();
return compareInstanceObjects(remoteUsers, localUsers, "email", "user");
} else {
log.info("Pulling users from instance...");
@@ -399,11 +402,31 @@ export async function pullInstanceUsers(preview: boolean = false) {
}
}
export async function readInstanceUsers() {
let localUsers: ExportedUser[] = [];
try {
localUsers = (await yamlParseFile("instance_users.yaml")) as ExportedUser[];
} catch {
log.warn("No instance_users.yaml file found");
}
return localUsers;
}
export async function readInstanceGroups() {
let localGroups: InstanceGroup[] = [];
try {
localGroups = (await yamlParseFile(
"instance_groups.yaml"
)) as ExportedInstanceGroup[];
} catch {
log.warn("No instance_groups.yaml file found");
}
return localGroups;
}
export async function pushInstanceUsers(preview: boolean = false) {
const remoteUsers = await wmill.globalUsersExport();
const localUsers = (await Deno.readTextFile("instance_users.yaml")
.then((raw) => yamlParse(raw))
.catch(() => [])) as ExportedUser[];
const localUsers: ExportedUser[] = await readInstanceUsers();
if (preview) {
return compareInstanceObjects(localUsers, remoteUsers, "email", "user");
@@ -421,11 +444,7 @@ export async function pullInstanceGroups(preview = false) {
const remoteGroups = await wmill.exportInstanceGroups();
if (preview) {
let localGroups: InstanceGroup[] = [];
try {
const raw = await Deno.readTextFile("instance_groups.yaml");
localGroups = yamlParse(raw) as InstanceGroup[];
} catch {}
const localGroups = await readInstanceGroups();
return compareInstanceObjects(remoteGroups, localGroups, "name", "group");
} else {
log.info("Pulling groups from instance...");
@@ -441,9 +460,7 @@ export async function pullInstanceGroups(preview = false) {
export async function pushInstanceGroups(preview: boolean = false) {
const remoteGroups = await wmill.exportInstanceGroups();
const localGroups = (await Deno.readTextFile("instance_groups.yaml")
.then((raw) => yamlParse(raw))
.catch(() => [])) as ExportedInstanceGroup[];
const localGroups = await readInstanceGroups();
if (preview) {
return compareInstanceObjects(localGroups, remoteGroups, "name", "group");
+4 -1
View File
@@ -55,7 +55,10 @@ export function deepEqual<T>(a: T, b: T): boolean {
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
if (a.toString !== Object.prototype.toString) {
if (
a.toString !== Object.prototype.toString &&
typeof a.toString == "function"
) {
return a.toString() === b.toString();
}
+5 -3
View File
@@ -4,11 +4,13 @@ ARG PYTHON_IMAGE=python:3.11.10-slim-bookworm
FROM ${RUST_IMAGE} AS rust_base
RUN yum install -y rust-toolset
RUN yum update -y && \
yum install -y git openssl-devel npm nodejs rustfmt
# Install rust manually
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo install cargo-chef --version ^0.1
WORKDIR /windmill
@@ -35,7 +37,7 @@ COPY /backend/parsers/windmill-parser-wasm/pkg/ /backend/parsers/windmill-parser
COPY /typescript-client/docs/ /frontend/static/tsdocs/
RUN npm run generate-backend-client
ENV NODE_OPTIONS "--max-old-space-size=8192"
ENV NODE_OPTIONS "--max-old-space-size=10240"
RUN npm run build
+10 -11
View File
@@ -3,17 +3,16 @@
This directory contains the Dockerfiles for building Windmill binaries for Red Hat Linux 9.
We build Windmill on the Red Hat Universal Base Image 9. Windmill requires the xmlsec1-devel package which is not available in the default UBI9 repositories. It is however included in the CodeReady Builder for RHEL9 repository which requires a RedHat subscription.
Moreover, only rust v1.75 is supported on Red Hat Linux 9. To make Windmill compatible with Rust v1.75, you need to pin the following libraries:
```
aws-config = "=1.4.0"
aws-sdk-sts = "=1.25.0"
aws-sdk-ssooidc = "=1.25.0"
aws-sdk-sso = "=1.25.0"
```
Make sure to include `aws-sdk-ssooidc` and `aws-sdk-sso` in the Cargo.toml of windmill-common as well to enforce the correct versions of the nested dependencies. Make them optional and include them in the `parquet` feature.
It's also possible that you need to add `#[async_recursion]` to the `lock_modules` function in the `backend/windmill-worker/src/worker_lockfiles.rs` file for it to compile.
Once the image is built, you can simply copy the binary on any Red Hat Linux 9 machine and run it. You will just need to install the xmlsec1 package which can be installed directly using `yum/dnf install xmlsec1`.
## Notes
- you will need to register on Red Hat and have an individual developer subscription and pass the username and password to docker build:
```
docker build \
-f docker/RHEL9/Dockerfile \
--build-arg features="$features" \
--secret id=rh_username,src=/path/to/rh_username \
--secret id=rh_password,src=/path/to/rh_password \
.
```
@@ -1,28 +0,0 @@
Error parsing /git/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (8350:3)
8347 | application/json:
8348 | schema: {}
8349 |
8350 | /w/{workspace}/job_helpers/loa ...
----------^
8351 | get:
8352 | summary: Load a preview of ...
ParserError: Error parsing /git/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (8350:3)
8347 | application/json:
8348 | schema: {}
8349 |
8350 | /w/{workspace}/job_helpers/loa ...
----------^
8351 | get:
8352 | summary: Load a preview of ...
at Object.parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parsers/yaml.js:44:23)
at getResult (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:116:22)
at runNextPlugin (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:64:32)
at /git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:55:9
at new Promise (<anonymous>)
at Object.run (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:54:12)
at parseFile (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:130:38)
at parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:56:30)
at async $RefParser.parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:115:28)
at async $RefParser.resolve (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:145:13)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.407.1",
"version": "1.409.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.407.1",
"version": "1.409.2",
"license": "AGPL-3.0",
"dependencies": {
"@aws-crypto/sha256-js": "^4.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.407.1",
"version": "1.409.2",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -141,6 +141,9 @@
defaultValue?: any,
nnullable?: boolean
) {
if (label == 'toString' && typeof value == 'function') {
value = undefined
}
if ((value == undefined || value == null) && !ignoreValueUndefined) {
value = defaultValue
if (defaultValue === undefined || defaultValue === null) {
@@ -6,6 +6,7 @@
import AssignableTagsInner from './AssignableTagsInner.svelte'
export let placement: 'bottom-end' | 'top-end' = 'bottom-end'
export let color: 'nord' | 'dark' = 'dark'
</script>
<Popup
@@ -13,7 +14,7 @@
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
>
<svelte:fragment slot="button">
<Button color="dark" size="xs" nonCaptureEvent={true}>
<Button {color} size="xs" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center"
><Pen size={14} /> Custom Tags&nbsp;<Tooltip light
>Tags are assigned to scripts and flows. Workers only accept jobs that correspond to their
@@ -52,6 +52,10 @@
>
<input type="text" placeholder="yourorg" bind:value={org} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
@@ -52,6 +52,10 @@
>
<input type="text" placeholder="yourorg" bind:value={org} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
+4 -5
View File
@@ -203,7 +203,7 @@
<div class="w-full flex space-x-8">
<div class="w-full flex flex-col gap-4">
<Label label="Cron">
<Label label="Cron" class="font-semibold" primary={true}>
<svelte:fragment slot="error">
{#if !validCRON}
<div class="text-red-600 text-xs"> Invalid cron syntax </div>
@@ -219,10 +219,10 @@
{disabled}
/>
</Label>
<Label label="Timezone">
<Label label="Timezone" class="font-semibold" primary>
{#if disabled}
<div>
<Badge>{timezone}</Badge>
<Badge><span class="text-primary dark:text-primary-inverse">{timezone}</span></Badge>
</div>
{:else}
<Select
@@ -390,9 +390,8 @@
</div>
<div class="w-full flex flex-col space-y-2">
<div class="text-sm font-semibold leading-none">Execution summary</div>
<div class="text-sm font-semibold leading-none">Estimated upcoming events ({timezone})</div>
<div class="flex flex-col space-y-2">
<div class="text-sm">Estimated upcoming events ({timezone})</div>
<div class="flex flex-col rounded-md p-4 border text-tertiary bg-surface-secondary gap-0.5">
{#each preview as date}
<span class="text-sm">{dateFormatter(new Date(date))}</span>
+14 -11
View File
@@ -15,7 +15,8 @@
WorkspaceService,
type InputTransform,
type RawScript,
type PathScript
type PathScript,
type TriggersCount
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { copilotInfo, userStore, workspaceStore } from '$lib/stores'
@@ -41,14 +42,13 @@
import { workspacedOpenai } from './copilot/lib'
import type { FlowCopilotContext, FlowCopilotModule } from './copilot/flow'
import { pickScript } from './flows/flowStateUtils'
import type { Schedule } from './flows/scheduleUtils'
import {
approximateFindPythonRelativePath,
isTypescriptRelativePath,
parseTypescriptDeps
} from '$lib/relative_imports'
import Tooltip from './Tooltip.svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
$: token = $page.url.searchParams.get('wm_token') ?? undefined
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
@@ -467,13 +467,7 @@
}
const flowStateStore = writable({} as FlowState)
const scheduleStore = writable<Schedule>({
args: {},
cron: '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: false,
summary: undefined
})
const previewArgsStore = writable<Record<string, any>>({})
const scriptEditorDrawer = writable(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
@@ -481,10 +475,19 @@
const testStepStore = writable<Record<string, any>>({})
const selectedIdStore = writable('settings-metadata')
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
const triggersCount = writable<TriggersCount | undefined>(undefined)
setContext<TriggerContext>('TriggerContext', {
primarySchedule: primaryScheduleStore,
selectedTrigger: selectedTriggerStore,
triggersCount: triggersCount
})
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
schedule: scheduleStore,
previewArgs: previewArgsStore,
scriptEditorDrawer,
moving,
@@ -559,6 +559,11 @@
<pre class="text-xs pt-2 whitespace-pre-wrap text-primary"
>{result.error.stack ?? ''}</pre
>
{#if result.error?.extra}
<pre class="text-xs pt-2 whitespace-pre-wrap text-primary"
>{JSON.stringify(result.error.extra, null, 4)}</pre
>
{/if}
<slot />
</div>
{#if language === 'bun'}
@@ -627,7 +632,7 @@
>
</button>
{:else if !result?.disable_download}
<FileDownload s3object={result} />
<FileDownload {workspaceId} s3object={result} />
<button
class="text-secondary underline text-2xs whitespace-nowrap"
on:click={() => {
+119 -75
View File
@@ -9,7 +9,8 @@
ScriptService,
type OpenFlow,
type RawScript,
type InputTransform
type InputTransform,
type TriggersCount
} from '$lib/gen'
import { initHistory, push, redo, undo } from '$lib/history'
import {
@@ -40,7 +41,6 @@
import { dfs, getPreviousIds } from './flows/previousResults'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext, FlowInput } from './flows/types'
import { cleanInputs, emptyFlowModuleState } from './flows/utils'
import {
@@ -82,6 +82,7 @@
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'
export let initialPath: string = ''
export let pathStoreInit: string | undefined = undefined
@@ -100,30 +101,47 @@
export let customUi: FlowBuilderWhitelabelCustomUi = {}
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
$: setContext('customUi', customUi)
const dispatch = createEventDispatcher()
async function createSchedule(path: string) {
const { cron, timezone, args, enabled, summary } = $scheduleStore
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
const triggersCount = writable<TriggersCount | undefined>(
savedPrimarySchedule
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
: undefined
)
try {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path: path,
schedule: formatCron(cron),
timezone,
script_path: path,
is_flow: true,
args,
enabled,
summary
}
})
} catch (err) {
sendUserToast(`The primary schedule could not be created: ${err}`, true)
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
primaryScheduleStore.set(schedule)
loadTriggers()
}
async function createSchedule(path: string) {
if ($primaryScheduleStore) {
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
try {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path: path,
schedule: formatCron(cron),
timezone,
script_path: path,
is_flow: true,
args,
enabled,
summary
}
})
} catch (err) {
sendUserToast(`The primary schedule could not be created: ${err}`, true)
}
} else {
sendUserToast('The primary schedule could not be created: no schedule data', true)
}
}
@@ -187,7 +205,8 @@
typ: 'flow',
value: {
...flow,
path: $pathStore
path: $pathStore,
primary_schedule: $primaryScheduleStore
}
}
})
@@ -241,7 +260,7 @@
// console.log('flow', computeUnlockedSteps(flow)) // del
// loadingSave = false // del
// return
const { cron, timezone, args, enabled, summary } = $scheduleStore
if (newFlow) {
try {
localStorage.removeItem('flow')
@@ -264,7 +283,7 @@
deployment_message: deploymentMsg || undefined
}
})
if (enabled) {
if ($primaryScheduleStore && $primaryScheduleStore.enabled) {
await createSchedule($pathStore)
}
} else {
@@ -284,31 +303,40 @@
workspace: $workspaceStore ?? '',
path: initialPath
})
if (
JSON.stringify(schedule.args) != JSON.stringify(args) ||
schedule.schedule != cron ||
schedule.timezone != timezone ||
schedule.summary != summary
) {
await ScheduleService.updateSchedule({
if ($primaryScheduleStore) {
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
if (
JSON.stringify(schedule.args) != JSON.stringify(args) ||
schedule.schedule != cron ||
schedule.timezone != timezone ||
schedule.summary != summary
) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore ?? '',
path: initialPath,
requestBody: {
schedule: formatCron(cron),
timezone,
args,
summary
}
})
}
if (enabled != schedule.enabled) {
await ScheduleService.setScheduleEnabled({
workspace: $workspaceStore ?? '',
path: initialPath,
requestBody: { enabled }
})
}
} else if (scheduleExists) {
await ScheduleService.deleteSchedule({
workspace: $workspaceStore ?? '',
path: initialPath,
requestBody: {
schedule: formatCron(cron),
timezone,
args,
summary
}
path: $pathStore
})
}
if (enabled != schedule.enabled) {
await ScheduleService.setScheduleEnabled({
workspace: $workspaceStore ?? '',
path: initialPath,
requestBody: { enabled }
})
}
} else if (enabled) {
} else if ($primaryScheduleStore && $primaryScheduleStore.enabled) {
await createSchedule(initialPath)
}
@@ -359,7 +387,8 @@
encodeState({
flow: $flowStore,
path: $pathStore,
selectedId: $selectedIdStore
selectedId: $selectedIdStore,
primarySchedule: $primaryScheduleStore
})
)
} catch (err) {
@@ -369,18 +398,14 @@
}
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
'webhooks'
)
export function getSelectedId() {
return $selectedIdStore
}
const scheduleStore = writable<Schedule>({
summary: undefined,
args: {},
cron: '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: false
})
const previewArgsStore = writable<Record<string, any>>(initialArgs)
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
@@ -395,10 +420,14 @@
selectedIdStore.set(selectedId)
}
function selectTrigger(selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes') {
selectedTriggerStore.set(selectedTrigger)
}
let insertButtonOpen = writable<boolean>(false)
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
schedule: scheduleStore,
previewArgs: previewArgsStore,
scriptEditorDrawer,
moving,
@@ -414,25 +443,31 @@
insertButtonOpen
})
async function loadSchedule() {
loadFlowSchedule(initialPath, $workspaceStore!)
.then((schedule: Schedule) => {
scheduleStore.set(schedule)
})
.catch(() => {
scheduleStore.set({
summary: undefined,
cron: '0 */5 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
args: {},
enabled: false
})
})
setContext<TriggerContext>('TriggerContext', {
selectedTrigger: selectedTriggerStore,
primarySchedule: primaryScheduleStore,
triggersCount
})
async function loadTriggers() {
$triggersCount = await FlowService.getTriggersCountOfFlow({
workspace: $workspaceStore!,
path: initialPath
})
if ($primaryScheduleStore && $triggersCount.primary_schedule == undefined) {
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: ($triggersCount.schedule_count ?? 0) + 1,
primary_schedule: {
schedule: $primaryScheduleStore.cron
}
}
}
}
$: selectedId && select(selectedId)
$: initialPath && initialPath != '' && $workspaceStore && loadSchedule()
$: initialPath && initialPath != '' && $workspaceStore && loadTriggers()
function onKeyDown(event: KeyboardEvent) {
let classes = event.target?.['className']
@@ -699,10 +734,13 @@
}
if (module.type === 'trigger') {
if (!$scheduleStore.cron) {
$scheduleStore.cron = '0 */15 * * *'
$primaryScheduleStore = {
summary: 'Scheduled poll of flow',
args: {},
cron: '0 */15 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: true
}
$scheduleStore.enabled = true
}
const flowModule: FlowModule & {
@@ -1152,7 +1190,7 @@
</div>
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
{#if $scheduleStore.enabled}
{#if $primaryScheduleStore != undefined ? $primaryScheduleStore && $primaryScheduleStore?.enabled : $triggersCount?.primary_schedule}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: Calendar }}
@@ -1160,10 +1198,15 @@
color="light"
size="xs"
on:click={async () => {
select('settings-schedule')
select('triggers')
selectTrigger('schedules')
}}
>
{$scheduleStore.cron ?? ''}
{$primaryScheduleStore != undefined
? $primaryScheduleStore
? $primaryScheduleStore?.cron
: ''
: $triggersCount?.primary_schedule?.schedule}
</Button>
{/if}
@@ -1334,6 +1377,7 @@
on:reload={() => {
renderCount += 1
}}
{newFlow}
/>
{:else}
<CenteredPage>Loading...</CenteredPage>
@@ -13,6 +13,7 @@
description?: string
value: FlowValue
schema?: any
path?: string
}
export let overflowAuto = false
@@ -32,20 +33,25 @@
class:overflow-auto={overflowAuto}
>
<FlowGraphV2
path={flow?.path}
{download}
minHeight={400}
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
preprocessorModule={flow?.value?.preprocessor_module}
on:select={(e) => {
if (e?.detail === 'failure') {
let nodeId = e?.detail
if (nodeId === 'triggers') {
dispatch('triggerDetail')
return
} else if (nodeId === 'failure') {
stepDetail = flow?.value?.failure_module
} else if (e?.detail === 'preprocessor') {
} else if (nodeId === 'preprocessor') {
stepDetail = flow?.value?.preprocessor_module
} else {
stepDetail = dfs(flow?.value?.modules ?? [], (m) => m).find((m) => m?.id === e?.detail)
}
stepDetail = stepDetail ?? e?.detail
stepDetail = stepDetail ?? nodeId
dispatch('select', stepDetail)
}}
/>
@@ -76,9 +76,8 @@
size="xs"
startIcon={{ icon: Clipboard }}
btnClasses="absolute top-2 right-2 w-min z-20"
>
Copy content
</Button>
iconOnly
/>
<div class={shouldDisplayLoadMore ? 'code-container' : ''}>
<Highlight
@@ -372,6 +372,10 @@
/>
</div>
<div class="p-2 border rounded">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={oauths[k]['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input
@@ -61,6 +61,10 @@
>
<input type="text" placeholder="Base URL" bind:value={baseUrl} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
@@ -52,6 +52,10 @@
>
<input type="text" placeholder="yourorg" bind:value={org} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
+2 -1
View File
@@ -1,11 +1,12 @@
<script lang="ts">
export let label: string | undefined = undefined
export let primary = false
</script>
<div class={$$props.class}>
<div class="flex flex-row justify-between items-center w-full">
<div class="flex flex-row items-center gap-1">
<span class="text-secondary text-sm leading-6">{label}</span>
<span class="{primary ? 'text-primary' : 'text-secondary'} text-sm leading-6">{label}</span>
<slot name="header" />
</div>
<slot name="error" />
+16 -8
View File
@@ -55,9 +55,14 @@
const providersType = providers.map((p) => p.type as string)
let showPassword = false
let logins: string[] | undefined = undefined
let logins: OAuthLogin[] | undefined = undefined
let saml: string | undefined = undefined
type OAuthLogin = {
type: string
displayName: string
}
async function login(): Promise<void> {
if (!email || !password) {
sendUserToast('Please fill in both email and password', true)
@@ -144,7 +149,10 @@
async function loadLogins() {
const allLogins = await OauthService.listOauthLogins()
logins = allLogins.oauth
logins = allLogins.oauth.map(login => ({
type: login.type,
displayName: login.display_name || login.type
}))
saml = allLogins.saml
showPassword = (logins.length == 0 && !saml) || (email != undefined && email.length > 0)
@@ -205,26 +213,26 @@
<Skeleton layout={[0.5, [2.375]]} />
{/each}
{:else}
{#each providers as { type, icon, name }}
{#if logins?.includes(type)}
{#each providers as { type, icon }}
{#if logins?.some(login => login.type === type)}
<Button
color="light"
variant="border"
startIcon={{ icon, classes: 'h-4' }}
on:click={() => storeRedirect(type)}
>
{name}
{logins.find(login => login.type === type)?.displayName}
</Button>
{/if}
{/each}
{#each logins.filter((x) => !providersType?.includes(x)) as login}
{#each logins.filter((login) => !providersType?.includes(login.type)) as login}
<Button
color="dark"
variant="border"
btnClasses="mt-2 w-full !border-gray-300"
on:click={() => storeRedirect(login)}
on:click={() => storeRedirect(login.type)}
>
{login}
{login.displayName}
</Button>
{/each}
{/if}
@@ -50,6 +50,10 @@
>
{#if enabled}
<div class="p-2 rounded border">
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Client Id</span>
<input type="text" placeholder="Client Id" bind:value={value['id']} />
@@ -66,6 +66,10 @@
</div>
</div>
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">Custom Name</span>
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
</label>
<label class="block pb-2">
<span class="text-primary font-semibold text-sm"
>Client Id <Tooltip
+47 -65
View File
@@ -24,8 +24,6 @@
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
import FolderEditor from './FolderEditor.svelte'
import { random_adj } from './random_positive_adjetive'
import Required from './Required.svelte'
import Tooltip from './Tooltip.svelte'
import { Eye, Folder, Plus, SearchCode, User } from 'lucide-svelte'
type PathKind =
@@ -162,7 +160,7 @@
.map((x) => ({
name: x,
write:
($userStore?.folders?.includes(x) == true ?? false) ||
$userStore?.folders?.includes(x) == true ||
($userStore?.is_admin ?? false) ||
($userStore?.is_super_admin ?? false)
}))
@@ -322,54 +320,54 @@
<div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 pb-0 mb-1">
{#if meta != undefined}
<div class="flex gap-x-4 shrink">
<!-- svelte-ignore a11y-label-has-associated-control -->
{#if !hideUser}
<div class="block">
<span class="text-secondary text-sm whitespace-nowrap">&nbsp;</span>
<ToggleButtonGroup
class="mt-0.5"
bind:selected={meta.ownerKind}
on:selected={(e) => {
setDirty()
const kind = e.detail
if (meta) {
if (kind === 'folder') {
meta.owner = folders?.[0]?.name ?? ''
} else if (kind === 'group') {
meta.owner = 'all'
} else {
meta.owner = $userStore?.username?.split('@')[0] ?? ''
}
<!-- svelte-ignore a11y-label-has-associated-control -->
{#if !hideUser}
<div class="block">
<ToggleButtonGroup
class="mt-0.5"
bind:selected={meta.ownerKind}
on:selected={(e) => {
setDirty()
const kind = e.detail
if (meta) {
if (kind === 'folder') {
meta.owner = folders?.[0]?.name ?? ''
} else if (kind === 'group') {
meta.owner = 'all'
} else {
meta.owner = $userStore?.username?.split('@')[0] ?? ''
}
}}
>
<ToggleButton
icon={User}
{disabled}
light
size="xs"
value="user"
position="left"
label="User"
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
icon={Folder}
{disabled}
light
size="xs"
value="folder"
position="right"
label="Folder"
/>
</ToggleButtonGroup>
</div>
{/if}
}
}}
>
<ToggleButton
icon={User}
{disabled}
light
size="xs"
value="user"
position="left"
label="User"
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
icon={Folder}
{disabled}
light
size="xs"
value="folder"
position="right"
label="Folder"
/>
</ToggleButtonGroup>
</div>
{/if}
{#if !hideUser}
<div class="text-xl">/</div>
{/if}
<div>
{#if meta.ownerKind === 'user'}
<label class="block shrink min-w-0">
<span class="text-secondary text-sm">User</span>
<input
class="!w-36"
type="text"
@@ -381,16 +379,6 @@
</label>
{:else if meta.ownerKind === 'folder'}
<label class="block grow w-48">
<span class="text-secondary text-sm">
Folder
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/groups_and_folders"
>
Read and write permissions are given to groups and users at the folder level and
shared by all items inside the folder.
</Tooltip>
</span>
<div class="flex flex-row items-center gap-1 w-full">
<select class="grow w-full" {disabled} bind:value={meta.owner}>
{#if folders?.length == 0}
@@ -426,14 +414,8 @@
</label>
{/if}
</div>
<span class="text-xl">/</span>
<label class="block grow w-full max-w-md">
<div class="text-secondary text-sm flex items-center gap-1 w-full justify-between">
<div>
Name
<Required required={true} />
</div>
<div class="text-2xs text-tertiary"> '/' for subfolders </div>
</div>
<!-- svelte-ignore a11y-autofocus -->
<input
{disabled}
@@ -0,0 +1,86 @@
<script lang="ts">
import { Badge, Button } from './common'
import { sendUserToast } from '$lib/toast'
import { base } from '$lib/base'
import Toggle from './Toggle.svelte'
import { ListOrdered, PenBox } from 'lucide-svelte'
import JobArgs from './JobArgs.svelte'
import Tooltip from './Tooltip.svelte'
import ScheduleEditor from './ScheduleEditor.svelte'
import { type Schedule } from '$lib/gen'
export let schedule: any
export let can_write: boolean
export let path: string
export let isFlow: boolean
export let scheduleEditor: ScheduleEditor
export let setScheduleEnabled: (path: string, enabled: boolean) => void
$: schedule = typeof schedule === 'boolean' ? undefined : (schedule as Schedule | undefined)
</script>
<div class="flex flex-col gap-2 grow w-full">
<div class="grid grid-cols-3 w-full">
<div class="flex justify-start">
<Badge color="indigo" small>
Primary&nbsp;
<Tooltip light>
Share the same path as the script or flow it is attached to and its path get renamed
whenever the source path is renamed
</Tooltip>
</Badge>
</div>
<div class="flex justify-center">
<div class="flex flex-row gap-2 items-center">
<input
size="9"
class="!text-xs !h-6 !text-primary"
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
value={schedule?.schedule ?? ''}
disabled={true}
/>
<Toggle
checked={schedule?.enabled ?? false}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
options={{
right: 'On'
}}
size="xs"
textClass="text-primary font-normal text-xs"
/>
</div>
</div>
<div class="flex justify-end">
<div class="flex flex-row gap-2 items-center">
<Button size={'xs'} variant="border" color="light" href={`${base}/runs/${path}`}>
<span>Runs</span>
<ListOrdered size={14} />
</Button>
<Button
size="xs"
color="dark"
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
>
<PenBox size={14} />
</Button>
</div>
</div>
</div>
{#if Object.keys(schedule?.args ?? {}).length > 0}
<div class="">
<JobArgs args={schedule?.args ?? {}} />
</div>
{:else}
<div class="text-xs text-tertiary"> No arguments </div>
{/if}
</div>
@@ -215,7 +215,7 @@
<div>
<form on:submit|preventDefault>
<table class="w-full border-collapse mb-2 text-xs table-auto">
<thead class="bg-gray-200 text-left text-xs">
<thead class="bg-gray-200 dark:bg-slate-600 text-left text-xs">
<tr>
<th class="p-2 min-w-[120px]">
Name
@@ -251,8 +251,9 @@
<tbody>
{#each alerts as alert, index}
<tr
class:staged-for-deletion={removedAlerts.includes(alert)}
class:non-interactable={removedAlerts.includes(alert)}
class={removedAlerts.includes(alert)
? 'bg-red-100 dark:bg-red-900 pointer-events-none opacity-50'
: ''}
>
<td class="border p-2">
{#if editingIndex === index}
@@ -265,12 +266,14 @@
{#if editingIndex === index}
<div class="flex flex-wrap gap-1 mb-2">
{#each alert.tags_to_monitor as tag}
<span class="inline-block bg-blue-100 rounded px-2 py-1 text-xs">
<span
class="inline-block bg-blue-100 dark:bg-blue-700 rounded px-2 py-1 text-xs"
>
{tag}
<button
on:click={() => removeTag(index, tag)}
aria-label="Remove Tag"
class="ml-1 text-black text-xs">x</button
class="ml-1 text-xs">x</button
>
</span>
{/each}
@@ -290,17 +293,23 @@
<!-- Add the new "Add All Tags" button here -->
<button
on:click={() => addAllTags(index)}
class="text-xs hover:bg-gray-200 rounded px-2 py-1 mt-1"
class="text-xs hover:bg-gray-200 dark:hover:bg-gray-700 rounded px-2 py-1 mt-1"
>
Add All Tags
</button>
{#if filteredTags.length > 0}
<ul class="autocomplete-list">
<ul
class="autocomplete-list border max-h-36 overflow-y-auto absolute z-50"
>
{#each filteredTags as tag}
<li>
<button type="button" on:click={() => addTag(index, tag)}
>{tag}</button
<button
type="button"
class="w-full text-left p-2 cursor-pointer hover:bg-slate-200 dark:hover:bg-slate-700"
on:click={() => addTag(index, tag)}
>
{tag}
</button>
</li>
{/each}
</ul>
@@ -308,7 +317,8 @@
{:else}
<div class="flex flex-wrap gap-1">
{#each alert.tags_to_monitor as tag}
<span class="inline-block bg-blue-100 rounded px-2 py-1 text-xs"
<span
class="inline-block bg-blue-100 dark:bg-blue-700 rounded px-2 py-1 text-xs"
>{tag}</span
>
{/each}
@@ -380,9 +390,14 @@
</Button>
</div>
{:else}
<p class="text-gray-600 text-sm">
Queue Metric Alerts are an enterprise feature allowing you to monitor queues for waiting jobs. Please upgrade to access this functionality.
<a href="https://www.windmill.dev/docs/misc/plans_details" target="_blank" class="text-blue-500 underline">Learn more about our plans.</a>
<p class="text-sm">
Queue Metric Alerts are an enterprise feature allowing you to monitor queues for waiting
jobs. Please upgrade to access this functionality.
<a
href="https://www.windmill.dev/docs/misc/plans_details"
target="_blank"
class="text-blue-500 underline">Learn more about our plans.</a
>
</p>
{/if}
</Section>
@@ -391,28 +406,3 @@
</div>
</DrawerContent>
</Drawer>
<style>
.staged-for-deletion {
background-color: #f8d7da; /* Light red background for staged deletion */
}
.non-interactable {
pointer-events: none; /* Disable pointer events */
opacity: 0.5; /* Reduce opacity */
}
.autocomplete-list {
border: 1px solid #ccc;
max-height: 150px;
overflow-y: auto;
background-color: white;
position: absolute;
z-index: 1000;
}
.autocomplete-list li {
padding: 8px;
cursor: pointer;
}
.autocomplete-list li:hover {
background-color: #f0f0f0;
}
</style>
@@ -249,6 +249,7 @@
if (canSelect) {
const ids = u.map((j) => data.datasets[j.datasetIndex].data[j.index].id)
selectedIds = ids
dispatch('pointClicked', ids)
}
},
@@ -1,178 +1,304 @@
<script lang="ts">
import ScheduleEditor from './ScheduleEditor.svelte'
import { Badge, Button } from './common'
import { Button } from './common'
import { workspaceStore } from '$lib/stores'
import { ScheduleService, type Schedule } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import { base } from '$lib/base'
import Toggle from './Toggle.svelte'
import { ListOrdered, Calendar, PenBox } from 'lucide-svelte'
import JobArgs from './JobArgs.svelte'
import { Calendar, Trash, Save } from 'lucide-svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Tooltip from './Tooltip.svelte'
import Label from '$lib/components/Label.svelte'
import { getContext } from 'svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
import CronInput from './CronInput.svelte'
import SchemaForm from './SchemaForm.svelte'
import { emptyString, sendUserToast } from '$lib/utils'
import Toggle from './Toggle.svelte'
export let schema: any
export let isFlow: boolean
export let path: string
export let can_write: boolean
export let newItem: boolean = false
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
let scheduleEditor: ScheduleEditor
let schedule: Schedule | false | undefined = undefined
$: loadSchedules(false) || path
let schedules: Schedule[] | undefined = undefined
$: path && loadSchedule()
$: path && loadSchedules()
async function loadSchedule() {
try {
let exists = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path
})
if (exists) {
schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path
})
} else {
schedule = false
let initialPrimarySchedule: ScheduleTrigger | false | undefined = undefined
async function loadSchedules(forceRefresh: boolean) {
if (!path || path == '') {
schedules = []
if ($primarySchedule == undefined) {
$primarySchedule = false
}
} catch (e) {
console.log('no primary schedule')
initialPrimarySchedule = structuredClone($primarySchedule)
return
}
}
async function loadSchedules() {
try {
schedules = (
await ScheduleService.listSchedules({
workspace: $workspaceStore ?? '',
path,
isFlow
})
).filter((s) => s.path != path)
} catch (e) {
console.error('impossible to load schedules')
}
}
async function setScheduleEnabled(path: string, enabled: boolean): Promise<void> {
try {
await ScheduleService.setScheduleEnabled({
path,
workspace: $workspaceStore!,
requestBody: { enabled }
const allSchedules = await ScheduleService.listSchedules({
workspace: $workspaceStore ?? '',
path: path,
isFlow
})
loadSchedule()
const primary = allSchedules.find((s) => s.path == path)
let remotePrimarySchedule = primary
? {
summary: primary.summary,
args: primary.args ?? {},
cron: primary.schedule,
timezone: primary.timezone,
enabled: primary.enabled
}
: false
if ($primarySchedule == undefined || forceRefresh) {
$primarySchedule = remotePrimarySchedule
}
initialPrimarySchedule = structuredClone(remotePrimarySchedule)
sendUserToast(`Schedule ${enabled ? 'enabled' : 'disabled'}`)
} catch (err) {
sendUserToast(`Cannot ` + (enabled ? 'disable' : 'enable') + ` schedule: ${err}`, true)
loadSchedule()
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: allSchedules.length,
primary_schedule: $primarySchedule ? { schedule: $primarySchedule.cron } : undefined
}
schedules = allSchedules.filter((s) => s.path != path)
} catch (e) {
console.error('impossible to load schedules', e)
}
}
async function save() {
const scheduleExists =
path != '' &&
!newItem &&
(await ScheduleService.existsSchedule({
workspace: $workspaceStore!,
path
}))
if (scheduleExists) {
console.log('primary schedule exists')
if ($primarySchedule) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore!,
path,
requestBody: {
summary: $primarySchedule.summary,
args: $primarySchedule.args,
schedule: $primarySchedule.cron,
timezone: $primarySchedule.timezone
}
})
sendUserToast(`Primary schedule updated`)
} else {
await ScheduleService.deleteSchedule({ workspace: $workspaceStore!, path })
sendUserToast(`Primary schedule deleted`)
}
} else {
if ($primarySchedule) {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path,
script_path: path,
is_flow: isFlow,
summary: $primarySchedule.summary,
args: $primarySchedule.args,
schedule: $primarySchedule.cron,
timezone: $primarySchedule.timezone,
enabled: $primarySchedule.enabled
}
})
sendUserToast(`Primary schedule created`)
}
}
loadSchedules(true)
}
</script>
<ScheduleEditor
on:update={() => {
loadSchedule()
loadSchedules()
loadSchedules(true)
}}
bind:this={scheduleEditor}
/>
<div class="p-2 flex flex-col">
<Button
on:click={() => scheduleEditor?.openNew(isFlow, path)}
variant="border"
color="light"
size="xs"
startIcon={{ icon: Calendar }}
>
New Schedule
</Button>
</div>
<div class="flex flex-col gap-4 w-full">
{#if $primarySchedule == undefined}
<Skeleton layout={[[12]]} />
{:else if $primarySchedule}
<div class="w-full flex flex-col mb-4">
{#if can_write}
<div class="w-full flex-row-reverse flex mb-2">
<div class="flex flex-row gap-4">
<Button
on:click={() => {
$primarySchedule = false
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: ($triggersCount?.schedule_count ?? 1) - 1,
primary_schedule: undefined
}
}}
variant="border"
color="light"
size="xs"
startIcon={{ icon: Trash }}
/>
{#if initialPrimarySchedule && !newItem}
<Toggle
disabled={emptyString($primarySchedule.cron)}
bind:checked={$primarySchedule.enabled}
options={{
right: 'Enabled'
}}
on:change={async (e) => {
if (!newItem) {
await ScheduleService.setScheduleEnabled({
path: path,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
{#if schedule}
<div class="p-2 flex flex-col gap-2">
<div class="flex flex-row justify-between h-8">
<div class="flex flex-row gap-2 items-center">
<input
class="inline-block !w-32"
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
value={schedule.schedule}
disabled={true}
/>
<div>
<Badge color="indigo" small
>Primary&nbsp;<Tooltip light
>Share the same path as the script or flow it is attached to and its path get renamed
whenever the source path is renamed</Tooltip
></Badge
>
</div>
</div>
<div class="flex flex-row gap-2 items-center">
<Toggle
checked={schedule.enabled}
on:change={(e) => {
if (can_write) {
setScheduleEnabled(path, e.detail)
} else {
sendUserToast('not enough permission', true)
}
}}
options={{
right: 'On'
}}
size="xs"
/>
<Button size="xs" variant="border" color="light" href={`${base}/runs/${path}`}>
<div class="flex flex-row gap-2">
<ListOrdered size={14} />
Runs
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${path}`)
}
}}
/>
{/if}
{#if !newItem}
<Button
on:click={save}
color="dark"
size="sm"
startIcon={{ icon: Save }}
disabled={JSON.stringify({ ...$primarySchedule, enabled: true }) ==
JSON.stringify({ ...initialPrimarySchedule, enabled: true })}
>Apply changes now</Button
>
{:else}
<div class="text-sm text-secondary mt-1 text-center"
>Deployed automatically with {isFlow ? 'flow' : 'script'}</div
>
{/if}
</div>
</Button>
<Button
size="xs"
color="dark"
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
>
<PenBox size={14} />
</Button>
</div>
</div>
{#if Object.keys(schedule?.args ?? {}).length > 0}
<div class="">
<JobArgs args={schedule.args ?? {}} />
</div>
{:else}
<div class="text-xs texg-gray-700"> No arguments </div>
{/if}
</div>
{:else if schedule == undefined}
<Skeleton layout={[[6]]} />
{/if}
<h3 class="px-2 pt-4">Other schedules</h3>
{#if schedules}
{#if schedules.length == 0}
<div class="text-xs text-secondary px-2"> No other schedules </div>
{:else}
<div class="flex flex-col divide-y px-2 pt-2">
{#each schedules as schedule (schedule.path)}
<div class="grid grid-cols-6 text-2xs items-center py-2"
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
<div>{schedule.enabled ? 'on' : 'off'}</div>
<button on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}>Edit</button>
</div>
{/each}
{/if}
<!-- svelte-ignore a11y-autofocus -->
<div class="mt-5">
<Label label="Summary" class="font-semibold" primary>
<input
autofocus
type="text"
placeholder="Short summary to be displayed when listed"
class="text-sm w-full"
bind:value={$primarySchedule.summary}
/>
</Label>
</div>
</div>
<CronInput bind:schedule={$primarySchedule.cron} bind:timezone={$primarySchedule.timezone} />
<SchemaForm {schema} bind:args={$primarySchedule.args} />
{#if emptyString($primarySchedule.cron)}
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
{/if}
{#if initialPrimarySchedule != false}
<div class="flex">
<Button size="xs" color="light" on:click={() => scheduleEditor?.openEdit(path, isFlow)}
>Advanced</Button
>
</div>
{/if}
{:else}
<div class="flex flex-row gap-4 mt-2">
<Button
on:click={() => {
$primarySchedule = {
summary: '',
args: {},
cron: '0 0 0 /1 * * *',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
enabled: true
}
$triggersCount = {
...($triggersCount ?? {}),
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
primary_schedule: { schedule: $primarySchedule.cron }
}
}}
variant="contained"
color="dark"
size="md"
startIcon={{ icon: Calendar }}
>
Set Primary Schedule
</Button>
{#if initialPrimarySchedule != undefined && initialPrimarySchedule != false && !newItem}
<Button on:click={save} color="dark" size="md" startIcon={{ icon: Save }}>
Apply changes now
</Button>
{:else}
<div class="text-sm text-center text-secondary mt-2"
>Deployed automatically with {isFlow ? 'flow' : 'script'}</div
>
{/if}
</div>
<Label label="Summary" class="font-semibold" primary>
<input
type="text"
disabled
placeholder="Short summary to be displayed when listed"
class="text-sm w-full"
/>
</Label>
<CronInput schedule={''} disabled timezone={Intl.DateTimeFormat().resolvedOptions().timeZone} />
<SchemaForm disabled {schema} />
{/if}
{:else}
<Skeleton layout={[[8]]} />
{/if}
{#if !newItem}
<div class="mt-10" />
{#if $primarySchedule}
<Button
on:click={() => scheduleEditor?.openNew(isFlow, path)}
variant="border"
color="light"
size="xs"
startIcon={{ icon: Calendar }}
>
New Schedule
</Button>
{/if}
<Label label="Other schedules">
{#if schedules}
{#if schedules?.length == 0 || schedules == undefined}
<div class="text-xs text-tertiary"> No other schedules </div>
{:else}
<div class="flex flex-col divide-y">
{#each schedules as schedule (schedule.path)}
<div class="grid grid-cols-6 text-xs items-center py-2">
<div class="col-span-3 truncate">{schedule.path}</div>
<div class="col-span-2 flex flex-row gap-4 flex-nowrap">
<div>{schedule.schedule}</div>
<div>{schedule.enabled ? 'on' : 'off'}</div>
</div>
<div class="flex justify-end">
<button
on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}
class="px-2">Edit</button
>
</div>
</div>
{/each}
</div>
{/if}
{:else}
<Skeleton layout={[[8]]} />
{/if}
</Label>
{/if}
</div>
@@ -520,13 +520,14 @@
<Toggle
disabled={!can_write}
checked={enabled}
options={{ right: 'enable', left: 'disable' }}
options={{ right: 'Enabled' }}
on:change={async (e) => {
await ScheduleService.setScheduleEnabled({
path: initialPath,
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
dispatch('update')
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
}}
/>
@@ -120,6 +120,7 @@
schema.properties = n
}
let nkeys = Object.keys(schema.properties ?? {})
if (!deepEqual(keys, nkeys)) {
keys = nkeys
dispatch('change')
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { emptySchema } from '$lib/utils'
import { copyToClipboard, emptySchema } from '$lib/utils'
import Highlight from 'svelte-highlight'
import json from 'svelte-highlight/languages/json'
@@ -10,6 +10,8 @@
import Tabs from './common/tabs/Tabs.svelte'
import { Badge } from './common'
import HighlightTheme from './HighlightTheme.svelte'
import Button from './common/button/Button.svelte'
import { ClipboardCopy } from 'lucide-svelte'
export let schema: Schema | undefined | any = emptySchema()
@@ -79,7 +81,19 @@
{/if}
</TabContent>
<TabContent value="advanced">
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
<div class="h-full relative">
<Button
wrapperClasses="absolute top-2 right-2 z-20"
on:click={() => copyToClipboard(JSON.stringify(schema, null, 4))}
color="light"
size="xs2"
startIcon={{
icon: ClipboardCopy
}}
iconOnly
/>
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
</div>
</TabContent>
</div>
</svelte:fragment>

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