Compare commits

..

107 Commits

Author SHA1 Message Date
Ruben Fiszel
f8f617f80b parallelize more stuff 2023-04-10 10:54:40 +02:00
Ruben Fiszel
e2f0584cea update 2023-04-10 09:14:13 +02:00
Ruben Fiszel
e1712e63a6 improve token lock 2023-04-10 08:00:41 +02:00
Faton Ramadani
2031e1ebd0 fix(frontend): fix popover configuration to avoid content shift (#1377) 2023-04-10 07:24:57 +02:00
Faton Ramadani
de8dc1e9cd fix(frontend): remove stopPropagation that was preventing components dnd (#1378) 2023-04-10 07:23:15 +02:00
Ruben Fiszel
de87d7ac27 fetch token in the background 2023-04-10 01:16:15 +02:00
Ruben Fiszel
2b003c684f make create_token_for_owner not a transaction anymore 2023-04-09 21:47:05 +02:00
Ruben Fiszel
bdf717c7b5 logs 2023-04-09 21:05:26 +02:00
Ruben Fiszel
3097510550 only register prometheus metrics if they are enabled 2023-04-09 15:39:25 +02:00
Ruben Fiszel
0c0b2d88cc deno optimization v0 2023-04-09 14:10:56 +02:00
Ruben Fiszel
1ffed41cf9 fix backend tests 2023-04-09 11:20:15 +02:00
Ruben Fiszel
dac61d1c98 feat(backend): extend cached resolution for go 2023-04-09 10:58:45 +02:00
Ruben Fiszel
facb67093c feat(python): cache dependency resolution 2023-04-08 23:25:46 +02:00
Ruben Fiszel
341a9662b7 add debug log line on execute 2023-04-08 22:21:22 +02:00
Ruben Fiszel
e80454e7fd chore(main): release 1.86.0 (#1352)
* chore(main): release 1.86.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-08 22:01:22 +02:00
Ruben Fiszel
3a232dbb57 feat(cli): add getFullResource 2023-04-08 21:57:40 +02:00
Ruben Fiszel
ee45f1ca7b show status code when possible 2023-04-08 20:09:27 +02:00
Ruben Fiszel
bbb6ee978d reduce verbosity of cache sync 2023-04-08 15:55:09 +02:00
Ruben Fiszel
5961995e80 reduce verbosity of cache sync 2023-04-08 15:48:07 +02:00
Ruben Fiszel
78f5fd275e remove deno nsjail in favor of deno sandboxing 2023-04-08 15:25:10 +02:00
Ruben Fiszel
a7c4c5d0a8 use skeleton for workers page 2023-04-08 14:08:56 +02:00
Ruben Fiszel
617220d75a update caddyfile 2023-04-08 14:00:51 +02:00
Ruben Fiszel
09042583c7 update self-host 2023-04-08 13:52:31 +02:00
Ruben Fiszel
2e80404e5e allow back navigation for home tabs 2023-04-08 13:20:46 +02:00
Ruben Fiszel
8ef29099f8 fix README and fix inline editor 2023-04-08 13:05:40 +02:00
Ruben Fiszel
1097dccfe5 change default recomputeOnInputChanges 2023-04-08 12:33:03 +02:00
Ádám Kovács
76a2a1db36 fix(frontend): Minor fixes (#1374)
* fixes

* reverse wm logo spin

* remove component tab label

* updates
2023-04-08 09:52:41 +02:00
Ruben Fiszel
65721b3b20 fix lsp relative imports 2023-04-08 00:11:10 +02:00
Ruben Fiszel
7675f08b7b feat(frontend): add impersonate api + local resolution of import by lsp v0 2023-04-07 22:54:55 +02:00
Ádám Kovács
b962ae3578 various app updates (#1373) 2023-04-07 13:58:02 +02:00
Ruben Fiszel
34a8b01b76 feat(frontend): add flow expand button 2023-04-07 12:36:29 +02:00
Ruben Fiszel
179382afbd improve further monaco assistant 2023-04-07 09:16:10 +02:00
Ruben Fiszel
37ee631363 experimental: make deno a separate language in the editor (#1370)
* all

* all

* all
2023-04-07 07:47:21 +02:00
Ruben Fiszel
dba37c2771 fix open drawer script builder 2023-04-07 01:44:36 +02:00
Ruben Fiszel
33f2bad8d9 add GOPATH to go mod tidy 2023-04-07 00:49:57 +02:00
Faton Ramadani
368cdefd91 Menu refactor (#1372)
* wip menu refactor

* wip

* wip

* feat(frontend): finish menu refactor

* feat(frontend): finish menu refactor
2023-04-07 00:08:53 +02:00
Ruben Fiszel
266b5b00da fix(frontend): make croninput a builder rather than a tab 2023-04-06 23:57:42 +02:00
Ruben Fiszel
8fe68c832b set schema on script template change 2023-04-06 23:28:03 +02:00
Faton Ramadani
92be102a07 feat(frontend): Improve login + toasts (#1363)
* feat(frontend): Improve login + toasts

* feat(frontend): Improve workspace selection

* feat(frontend): restore package.lock

* feat(frontend): restore package.lock

* feat(frontend): Set gray background + smal fixes

* feat(frontend): fix login modal margin

* feat(frontend): update color
2023-04-06 17:23:58 +02:00
Faton Ramadani
a344928f25 fix(frontend): Fix button poppup (#1368)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-06 16:09:26 +02:00
Faton Ramadani
f214d5f96b feat(frontend): Tone down text + display whole text (#1366) 2023-04-06 16:07:34 +02:00
Faton Ramadani
4af39f081b fix(frontend): Fix connected property (#1371)
* fix(frontend): Fix connected property

* fix(frontend): fix connected property

* fix(frontend): fix connected property
2023-04-06 16:02:10 +02:00
Ruben Fiszel
3a6b655ba8 fix server starting failed 2023-04-06 11:46:47 +02:00
Ruben Fiszel
2f156d09bf fix loading script from hub 2023-04-06 09:13:15 +02:00
Ruben Fiszel
b84be60c53 feat(frontend): make script editor a single page 2023-04-06 01:00:42 +02:00
Ruben Fiszel
eef9017a05 remove import from template 2023-04-05 22:14:15 +02:00
Ruben Fiszel
eff61bb8d3 fix(backend): allow longer name/company 2023-04-05 22:01:18 +02:00
Ruben Fiszel
8a594a89ad fix(backend): allow cors 2023-04-05 20:36:01 +02:00
Ruben Fiszel
fb60768cf3 fix star hover issue 2023-04-05 19:38:20 +02:00
Ruben Fiszel
8f7a11b896 feat(frontend): add workspace to ctx 2023-04-05 19:31:12 +02:00
Ruben Fiszel
0b4da1a97c minor schedule nits 2023-04-05 19:21:13 +02:00
Ruben Fiszel
f6d14f7fc3 minor schedule nits 2023-04-05 19:16:00 +02:00
Ruben Fiszel
449e7de71a fix search outputs in apps with null object 2023-04-05 18:53:57 +02:00
Ruben Fiszel
95ed99a1d2 fix search outputs in apps with null object 2023-04-05 18:52:41 +02:00
Ruben Fiszel
8c72722710 fix(backend): inline script app python fix 2023-04-05 18:26:27 +02:00
Oliver Veal
17176bb8d1 feat: improved cron/schedule editor (#1362)
* basic cron schedule editing ui

* schedules run in a user-specified timezone

* fix other uses of CronInput component

* use now() from database to schedule next job

* offset -> IANA timezone conversion on db migration

* sqlx ci

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-05 16:37:24 +02:00
Ruben Fiszel
4fb7468cf3 do no keep tar around and move it from the tmp folder 2023-04-05 15:02:37 +02:00
Faton Ramadani
51fc436456 fix(frontend): Fix flow templateEditor (#1367) 2023-04-05 13:06:56 +02:00
Ruben Fiszel
7f9050b285 feat(backend): lowercase all emails in relevant endpoints (#1361)
* all

* also modify invite_user
2023-04-05 11:36:17 +02:00
Ruben Fiszel
94eecea02b feat(backend): add /ready endpoint for workers 2023-04-05 10:34:25 +02:00
jneeee
4ec035b09a fix: no need to map internal ports to hosts (#1365) 2023-04-05 08:01:43 +02:00
Ruben Fiszel
922682c4d0 initialize cache if from tar 2023-04-04 16:31:47 +02:00
Faton Ramadani
831ff60bdf App input (#1353)
* feat(frontend): record frontend errors

* feat(frontend): display result

* feat(frontend): Fix name shadowing

* feat(frontend): fix typo

* fix(frontend): better display frontend errors

* fix(frontend): wip

* wip

* feat(frontend): text input

* feat(frontend): enable double click to open + disabled mode when input is computed or connected

* feat(frontend): fix monaco setCode

* feat(frontend): revert package.json changes

* feat(frontend): fix syncing issues

* feat(frontend): adapt style

* feat(frontend): fix event propagation
2023-04-04 16:00:50 +02:00
Ruben Fiszel
b86ca29fde fix polar handling 2023-04-04 14:25:27 +02:00
Ruben Fiszel
15c75d9d00 feat(backend): add GET endpoint to trigger scripts 2023-04-04 13:56:55 +02:00
Ruben Fiszel
096bf2022c kill the background bucket sync if necessary 2023-04-04 11:33:07 +02:00
Ruben Fiszel
bc4dd0eeaa only sync after the cache has been initialized 2023-04-04 11:03:25 +02:00
Ruben Fiszel
ae219eb3be create the first dirs properly 2023-04-04 09:29:08 +02:00
Ruben Fiszel
b851a5c65a first sync of the cache is at least after the first sync duration 2023-04-04 09:06:19 +02:00
Ruben Fiszel
bc36f5b309 log the entirecache.tar length 2023-04-04 08:32:20 +02:00
Ruben Fiszel
5b0a4d7838 only copy cache from bucket in background the first time 2023-04-04 08:24:02 +02:00
dependabot[bot]
f358aa5fe2 chore(deps-dev): bump cssnano from 5.1.15 to 6.0.0 in /frontend (#1359)
Bumps [cssnano](https://github.com/cssnano/cssnano) from 5.1.15 to 6.0.0.
- [Release notes](https://github.com/cssnano/cssnano/releases)
- [Commits](https://github.com/cssnano/cssnano/compare/cssnano@5.1.15...cssnano@6.0.0)

---
updated-dependencies:
- dependency-name: cssnano
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-04 01:34:22 +02:00
Ruben Fiszel
3e5ff8682a feat: do cache bucket syncing in background + check tar before pushing it (#1360)
* all

* improve tar caching
2023-04-04 01:34:11 +02:00
dependabot[bot]
0cbefd8214 chore(deps-dev): bump @sveltejs/adapter-static in /frontend (#1357)
Bumps [@sveltejs/adapter-static](https://github.com/sveltejs/kit/tree/HEAD/packages/adapter-static) from 1.0.6 to 2.0.1.
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/master/packages/adapter-static/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/adapter-static@2.0.1/packages/adapter-static)

---
updated-dependencies:
- dependency-name: "@sveltejs/adapter-static"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 21:07:56 +02:00
Ruben Fiszel
517b2c9cca fix(backend): always flush bash output 2023-04-03 20:07:43 +02:00
Ruben Fiszel
7a9091fed6 fix(backend): always flush bash output 2023-04-03 19:56:55 +02:00
Faton Ramadani
2819b09ce5 fix(frontend): Add FlowGraph display on Safari (#1351) 2023-04-03 16:39:57 +02:00
Ruben Fiszel
ef0165e419 chore(main): release 1.85.0 (#1348)
* chore(main): release 1.85.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-04-03 13:30:18 +02:00
Ruben Fiszel
be97be2c58 update lsp to not report imports errors 2023-04-03 13:26:00 +02:00
Ádám Kovács
daf827666b fix(frontend): PDF reader header positioning (#1350) 2023-04-03 11:56:38 +02:00
Ruben Fiszel
8c53598aba fix(backend): improve handling subflow with many depth using tailrec 2023-04-03 08:37:24 +02:00
Ruben Fiszel
6f33d549f0 improve mem handling for update_flow_status 2023-04-03 01:13:39 +02:00
Ruben Fiszel
390a988d4c fix(backend): improve subflow processing 2023-04-03 00:32:13 +02:00
Ruben Fiszel
8a8316c316 remove unecessary debug inputtransform 2023-04-02 22:20:05 +02:00
Ruben Fiszel
c638c511ca set ready to undefined for agGridTable 2023-04-02 18:29:49 +02:00
dependabot[bot]
59403fbe5d chore(deps): bump actions/setup-go from 3 to 4 (#1309)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 17:42:56 +02:00
Ruben Fiszel
7eed0b4666 update highlight + fix transform editor suggestions 2023-04-02 17:41:43 +02:00
Ruben Fiszel
4127ffe00c chore: update search library 2023-04-02 17:16:23 +02:00
Ruben Fiszel
3d9dfa645b update svelte-awsome 2023-04-02 17:08:37 +02:00
Ruben Fiszel
7c0de93b3d remove all frontend warnings and unused imports 2023-04-02 17:02:57 +02:00
Ruben Fiszel
de1e29492c feat(frontend): add agGrid api hooks + ready 2023-04-02 16:21:04 +02:00
Ádám Kovács
0b8a08cb49 feat(frontend): Add ID renaming popup (#1344)
* feat(frontend): Add id renaming popup

* fix(frontend): State reset

* actually do it

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-04-02 15:46:24 +02:00
Ruben Fiszel
4f7c45118b remove backend warnings & fix test 2023-04-02 13:56:16 +02:00
Ruben Fiszel
cdbab5c807 remove all frontend warnings 2023-04-02 13:52:15 +02:00
Ruben Fiszel
d4927cf757 signatures that cannot be parsed return 200 2023-04-02 12:47:38 +02:00
Ruben Fiszel
492f22526a refresh folders more aggressively in case a non non just created one 2023-04-02 12:08:52 +02:00
Ruben Fiszel
89c2fb41dd can read all folders if admin or superadmin + refresh user in background 2023-04-02 10:35:22 +02:00
Ruben Fiszel
018b051781 feat: add local cache for folder path used + invalidate cache on folder creation 2023-04-01 19:17:29 +02:00
Ruben Fiszel
c19be7a2fa ci: extract to s3 directly 2023-04-01 15:38:58 +02:00
Ruben Fiszel
aa3a3f6612 ci: extract to s3 directly 2023-04-01 15:36:26 +02:00
Ruben Fiszel
5b8c6bb35d fix compile 2023-04-01 15:07:32 +02:00
Ruben Fiszel
8d487c0ddb fix lack of suggestions in flow editor 2023-04-01 14:53:45 +02:00
Ruben Fiszel
efea19496f update vite and sveltekit 2023-04-01 13:22:12 +02:00
Ruben Fiszel
ab99950c5d fix cycle in imports 2023-04-01 01:40:19 +02:00
Ruben Fiszel
2062dc6c44 chore(main): release 1.84.1 (#1345)
* chore(main): release 1.84.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-31 21:07:04 +02:00
Ruben Fiszel
b6d5eef547 fix(cli): overwrite instead of smart diff 2023-03-31 21:02:37 +02:00
Ruben Fiszel
46d2c86b37 when pulling, overwrite or not but do not merge 2023-03-31 21:01:40 +02:00
285 changed files with 250361 additions and 10934 deletions

7
.env
View File

@@ -1,2 +1,7 @@
DB_PASSWORD=changeme
WM_BASE_URL=localhost
# this is the url that your instance is publicly exposed to
WM_BASE_URL=http://localhost
# this is the url that caddy will reverse proxy from. It might be different than WM_BASE_URL if you re using a second proxy/load balancer in front
CADDY_REVERSE_PROXY=http://localhost

View File

@@ -1,32 +0,0 @@
name: Deploy frontend to s3
on:
push:
branches: [main]
tags: ["*"]
concurrency:
group: ${{ github.ref }}-npmbuild
cancel-in-progress: true
jobs:
npm_build:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: "npm build"
timeout-minutes: 15
run:
cd frontend && npm ci && npm run generate-backend-client &&
NODE_OPTIONS="--max-old-space-size=8192" npm run build
- uses: reggionick/s3-deploy@v3
with:
folder: frontend/build
bucket: windmill-frontend
bucket-region: us-east-1

View File

@@ -18,7 +18,7 @@ permissions:
contents: read
id-token: write
packages: write
jobs:
build:
runs-on: ubuntu-22.04
@@ -50,7 +50,6 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Build and push publicly
uses: depot/build-push-action@v1
with:
@@ -63,7 +62,7 @@ jobs:
labels: |
${{ steps.meta-public.outputs.labels }}
org.opencontainers.image.licenses=AGPLv3
build_ee:
runs-on: ubuntu-22.04
steps:
@@ -142,7 +141,28 @@ jobs:
# run: docker kill ${{ steps.docker-container.outputs.id }}
# if: always()
deploy_s3:
needs: [build_ee]
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: shrink/actions-docker-extract@v2
id: extract
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:latest
path: "/static_frontend/."
- uses: reggionick/s3-deploy@v3
with:
folder: ${{ steps.extract.outputs.destination }}
bucket: windmill-frontend
bucket-region: us-east-1
publish_privately_heavy:
needs: [build_ee]
runs-on: [self-hosted, new]
@@ -191,8 +211,12 @@ jobs:
tags: |
${{ steps.meta-heavy.outputs.tags }}
labels: ${{ steps.meta-heavy.outputs.labels }}
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-heavy:buildcache,mode=max
cache-from:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-heavy:buildcache
cache-to:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-heavy:buildcache,mode=max
publish_privately_helm:
runs-on: [self-hosted, new]
@@ -204,7 +228,6 @@ jobs:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to registry
uses: docker/login-action@v2
@@ -220,7 +243,7 @@ jobs:
registry: ${{ env.ECR_REGISTRY }}
username: ${{ secrets.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Build and push privately
uses: docker/build-push-action@v4
if: github.event_name != 'pull_request'
@@ -230,5 +253,9 @@ jobs:
file: ./docker/DockerfileHelm
tags: |
${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:helm
cache-from: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache
cache-to: type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME }}-helm:buildcache,mode=max
cache-from:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-helm:buildcache
cache-to:
type=registry,ref=${{ env.LOCAL_REGISTRY }}/${{ env.IMAGE_NAME
}}-helm:buildcache,mode=max

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
- name: generate_go
run: |
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0

View File

@@ -1,6 +1,63 @@
# Changelog
## [1.86.0](https://github.com/windmill-labs/windmill/compare/v1.85.0...v1.86.0) (2023-04-08)
### Features
* **backend:** add /ready endpoint for workers ([94eecea](https://github.com/windmill-labs/windmill/commit/94eecea02b6295ad5674db4b010bf6ab7984fa17))
* **backend:** add GET endpoint to trigger scripts ([15c75d9](https://github.com/windmill-labs/windmill/commit/15c75d9d00a69ae97123ed371b9657e298345bdb))
* **backend:** lowercase all emails in relevant endpoints ([#1361](https://github.com/windmill-labs/windmill/issues/1361)) ([7f9050b](https://github.com/windmill-labs/windmill/commit/7f9050b285cf8f7f6baf05452b673f58988c452c))
* **cli:** add getFullResource ([3a232db](https://github.com/windmill-labs/windmill/commit/3a232dbb5792c28b26747e1ba260fffcdd4a8416))
* do cache bucket syncing in background + check tar before pushing it ([#1360](https://github.com/windmill-labs/windmill/issues/1360)) ([3e5ff86](https://github.com/windmill-labs/windmill/commit/3e5ff8682a298ba9e59b2662c4c04c5698447204))
* **frontend:** add flow expand button ([34a8b01](https://github.com/windmill-labs/windmill/commit/34a8b01b762c0b210d76101e7da7bd2397258e8d))
* **frontend:** add impersonate api + local resolution of import by lsp v0 ([7675f08](https://github.com/windmill-labs/windmill/commit/7675f08b7bfe319e496a86a7ef1ab7cc8c1d12d2))
* **frontend:** add workspace to ctx ([8f7a11b](https://github.com/windmill-labs/windmill/commit/8f7a11b8964e2c3405ce3689f9cf2298f9e71c75))
* **frontend:** Improve login + toasts ([#1363](https://github.com/windmill-labs/windmill/issues/1363)) ([92be102](https://github.com/windmill-labs/windmill/commit/92be102a070b1f17b9d3e40524cd21b54301b5a7))
* **frontend:** make script editor a single page ([b84be60](https://github.com/windmill-labs/windmill/commit/b84be60c53ca1ef65826123f39099d33c1f549c0))
* **frontend:** Tone down text + display whole text ([#1366](https://github.com/windmill-labs/windmill/issues/1366)) ([f214d5f](https://github.com/windmill-labs/windmill/commit/f214d5f96b6ac26cd3ef90a6ab696a6dfe02b3f0))
* improved cron/schedule editor ([#1362](https://github.com/windmill-labs/windmill/issues/1362)) ([17176bb](https://github.com/windmill-labs/windmill/commit/17176bb8d112b35228ce9183f4b2f81abe9e5b6e))
### Bug Fixes
* **backend:** allow cors ([8a594a8](https://github.com/windmill-labs/windmill/commit/8a594a89adba9915508884f900f58c4ab53cdfec))
* **backend:** allow longer name/company ([eff61bb](https://github.com/windmill-labs/windmill/commit/eff61bb8d3496bc1c5be4b1051f99ed4470a47ff))
* **backend:** always flush bash output ([517b2c9](https://github.com/windmill-labs/windmill/commit/517b2c9cca54628c8ee692d65c05bc2513eaaf22))
* **backend:** always flush bash output ([7a9091f](https://github.com/windmill-labs/windmill/commit/7a9091fed6aa99201b75bab88d4faddbe041eee4))
* **backend:** inline script app python fix ([8c72722](https://github.com/windmill-labs/windmill/commit/8c72722710db8e3720b01180b504cbc66e79f5ca))
* **frontend:** Add FlowGraph display on Safari ([#1351](https://github.com/windmill-labs/windmill/issues/1351)) ([2819b09](https://github.com/windmill-labs/windmill/commit/2819b09ce5011a467e994ee8b1f09cf33145003d))
* **frontend:** Fix button poppup ([#1368](https://github.com/windmill-labs/windmill/issues/1368)) ([a344928](https://github.com/windmill-labs/windmill/commit/a344928f251d697f53e40c517b0b86bd90e0ad52))
* **frontend:** Fix connected property ([#1371](https://github.com/windmill-labs/windmill/issues/1371)) ([4af39f0](https://github.com/windmill-labs/windmill/commit/4af39f081bf3d07aaade39e5a5a221741fe8f973))
* **frontend:** Fix flow templateEditor ([#1367](https://github.com/windmill-labs/windmill/issues/1367)) ([51fc436](https://github.com/windmill-labs/windmill/commit/51fc436456104c2d6a3cd6f6d62f08929e40d450))
* **frontend:** make croninput a builder rather than a tab ([266b5b0](https://github.com/windmill-labs/windmill/commit/266b5b00da3bd7643eaa5dba1b8c1456f11c5e30))
* **frontend:** Minor fixes ([#1374](https://github.com/windmill-labs/windmill/issues/1374)) ([76a2a1d](https://github.com/windmill-labs/windmill/commit/76a2a1db363facbaf9a0e9618f169d6cc66e946f))
* no need to map internal ports to hosts ([#1365](https://github.com/windmill-labs/windmill/issues/1365)) ([4ec035b](https://github.com/windmill-labs/windmill/commit/4ec035b09a58f8859bc576b03c24cc73f335f32d))
## [1.85.0](https://github.com/windmill-labs/windmill/compare/v1.84.1...v1.85.0) (2023-04-03)
### Features
* add local cache for folder path used + invalidate cache on folder creation ([018b051](https://github.com/windmill-labs/windmill/commit/018b051781e3f40b9d1da8ccdd5edb1cd49877ba))
* **frontend:** add agGrid api hooks + ready ([de1e294](https://github.com/windmill-labs/windmill/commit/de1e29492c9aefdfc59f605ba81f7c51a96bf2f3))
* **frontend:** Add ID renaming popup ([#1344](https://github.com/windmill-labs/windmill/issues/1344)) ([0b8a08c](https://github.com/windmill-labs/windmill/commit/0b8a08cb49644da7c354c3631751e925ac5353b9))
### Bug Fixes
* **backend:** improve handling subflow with many depth using tailrec ([8c53598](https://github.com/windmill-labs/windmill/commit/8c53598aba3fb89f4174d1c0ab3912096ac07c96))
* **backend:** improve subflow processing ([390a988](https://github.com/windmill-labs/windmill/commit/390a988d4c96256a4fbd6a9302fc47a5648c2c43))
* **frontend:** PDF reader header positioning ([#1350](https://github.com/windmill-labs/windmill/issues/1350)) ([daf8276](https://github.com/windmill-labs/windmill/commit/daf827666b13917f8c9abeab5bb2b072bd0fef0b))
## [1.84.1](https://github.com/windmill-labs/windmill/compare/v1.84.0...v1.84.1) (2023-03-31)
### Bug Fixes
* **cli:** overwrite instead of smart diff ([b6d5eef](https://github.com/windmill-labs/windmill/commit/b6d5eef5479e38cc36af2db67d4c45f78c622b9a))
## [1.84.0](https://github.com/windmill-labs/windmill/compare/v1.83.1...v1.84.0) (2023-03-31)

View File

@@ -1,15 +1,5 @@
{
auto_https off
}
http://{$BASE_URL} {
bind {$ADDRESS}
{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001
reverse_proxy /* http://windmill_server:8000
https://{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001
}
}
}

View File

@@ -124,6 +124,7 @@ ENV TZ=Etc/UTC
RUN /usr/local/bin/python3 -m pip install pip-tools
COPY --from=frontend /frontend/build /static_frontend
COPY --from=builder /windmill/target/release/windmill ${APP}/windmill
COPY --from=nsjail /nsjail/nsjail /bin/nsjail

View File

@@ -186,14 +186,19 @@ compiling from source or using without a postgres super user, see
### Docker compose
`docker compose up` with the following docker-compose is sufficient:
<https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml>
```
curl https://github.com/windmill-labs/windmill/blob/main/docker-compose.yml -o docker-compose.yml
curl https://github.com/windmill-labs/windmill/blob/main/CaddyFile -o Caddyfile
curl https://github.com/windmill-labs/windmill/blob/main/.env -o .env
docker compose up -d --pull always
```
Go to http://localhost et voilà :)
The default super-admin user is: admin@windmill.dev / changeme
From there, you can create other users (do not forget to change the password!)
From there, you can follow the setup app and creat other users.
### Kubernetes (k8s) and Helm charts

383
backend/Cargo.lock generated
View File

@@ -36,7 +36,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.9",
"once_cell",
"version_check",
]
@@ -214,7 +214,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -259,7 +259,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -537,6 +537,28 @@ dependencies = [
"winapi",
]
[[package]]
name = "chrono-tz"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
[[package]]
name = "cipher"
version = "0.3.0"
@@ -579,7 +601,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -615,9 +637,9 @@ dependencies = [
[[package]]
name = "concurrent-queue"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
dependencies = [
"crossbeam-utils",
]
@@ -678,9 +700,9 @@ dependencies = [
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
@@ -794,7 +816,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -811,7 +833,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -863,9 +885,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]]
name = "deno_core"
version = "0.177.0"
version = "0.179.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1563a04f319904eb5c4c981b0a5475a4258aa72d4576296f097a4f23b20f8428"
checksum = "8c9307ca2299cb7b0bdaa345cbdc82a252a8e4e5a4463e28f44c715d55e460fb"
dependencies = [
"anyhow",
"bytes",
@@ -888,9 +910,9 @@ dependencies = [
[[package]]
name = "deno_ops"
version = "0.55.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "769968c69c652009e43cb9e63f610a1cc3d0316c3fc59d7327f51089f65a70f4"
checksum = "04610f07342fbb33a2b7ea7aa16a95ab71adb13a0ce858a8d1a1414660a83e3e"
dependencies = [
"once_cell",
"pmutil",
@@ -1069,13 +1091,13 @@ dependencies = [
[[package]]
name = "errno"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1105,14 +1127,14 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.20"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.2.16",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1244,9 +1266,9 @@ checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
[[package]]
name = "futures-lite"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand",
"futures-core",
@@ -1265,7 +1287,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -1330,9 +1352,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
dependencies = [
"cfg-if",
"libc",
@@ -1374,6 +1396,16 @@ dependencies = [
"url",
]
[[package]]
name = "gosyn"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1584c4cbcb6f1d97481c19d1951c64f6e1256e5a03c28159ae45431c00511ee7"
dependencies = [
"strum",
"unic-ucd-category",
]
[[package]]
name = "h2"
version = "0.3.16"
@@ -1602,9 +1634,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.54"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d"
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1674,13 +1706,13 @@ dependencies = [
[[package]]
name = "io-lifetimes"
version = "1.0.9"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1704,14 +1736,14 @@ dependencies = [
[[package]]
name = "is-terminal"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1878,9 +1910,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.140"
version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
[[package]]
name = "libgit2-sys"
@@ -1992,6 +2024,12 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "matchit"
version = "0.7.0"
@@ -2120,6 +2158,7 @@ dependencies = [
"autocfg",
"num-integer",
"num-traits",
"rand 0.8.5",
"serde",
]
@@ -2215,9 +2254,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.48"
version = "0.10.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2"
checksum = "4d2f106ab837a24e03672c59b1239669a0596406ff657c3c0835b6b7f0f35a33"
dependencies = [
"bitflags",
"cfg-if",
@@ -2230,13 +2269,13 @@ dependencies = [
[[package]]
name = "openssl-macros"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.13",
]
[[package]]
@@ -2247,11 +2286,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.83"
version = "0.9.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b"
checksum = "3a20eace9dc2d82904039cb76dcf50fb1a0bba071cfd1629720b5d6f1ddba0fa"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
@@ -2266,9 +2304,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e"
[[package]]
name = "parking_lot"
@@ -2318,6 +2356,15 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
"regex",
]
[[package]]
name = "password-hash"
version = "0.5.0"
@@ -2548,9 +2595,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.54"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [
"unicode-ident",
]
@@ -2558,7 +2605,7 @@ dependencies = [
[[package]]
name = "progenitor"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#e04bb171f6b0016b4d1ec4e1ebb42f07277d2be2"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"anyhow",
"built",
@@ -2576,7 +2623,7 @@ dependencies = [
[[package]]
name = "progenitor-client"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#e04bb171f6b0016b4d1ec4e1ebb42f07277d2be2"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"bytes",
"futures-core",
@@ -2590,7 +2637,7 @@ dependencies = [
[[package]]
name = "progenitor-impl"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#e04bb171f6b0016b4d1ec4e1ebb42f07277d2be2"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"getopts",
"heck",
@@ -2603,7 +2650,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"syn 2.0.12",
"syn 2.0.13",
"thiserror",
"typify",
"unicode-ident",
@@ -2612,7 +2659,7 @@ dependencies = [
[[package]]
name = "progenitor-macro"
version = "0.2.1-dev"
source = "git+https://github.com/oxidecomputer/progenitor#e04bb171f6b0016b4d1ec4e1ebb42f07277d2be2"
source = "git+https://github.com/oxidecomputer/progenitor#3a5fe998e6c790223a9ff195df96e1f34ba2c149"
dependencies = [
"openapiv3",
"proc-macro2",
@@ -2623,7 +2670,7 @@ dependencies = [
"serde_json",
"serde_tokenstream",
"serde_yaml",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -2723,7 +2770,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.9",
]
[[package]]
@@ -2759,7 +2806,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.9",
"redox_syscall 0.2.16",
"thiserror",
]
@@ -2960,16 +3007,16 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.5"
version = "0.37.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e78cc525325c06b4a7ff02db283472f3c042b7ff0c391f96c6d5ac6f4f91b75"
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -2996,7 +3043,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#07bad48dbc47a910e853723c33183874f390e969"
source = "git+https://github.com/RustPython/RustPython#dde92d1269b5fbd09cf5dba5e7817f0e2a96c49f"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -3005,7 +3052,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#07bad48dbc47a910e853723c33183874f390e969"
source = "git+https://github.com/RustPython/RustPython#dde92d1269b5fbd09cf5dba5e7817f0e2a96c49f"
dependencies = [
"bitflags",
"bstr",
@@ -3018,7 +3065,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#07bad48dbc47a910e853723c33183874f390e969"
source = "git+https://github.com/RustPython/RustPython#dde92d1269b5fbd09cf5dba5e7817f0e2a96c49f"
dependencies = [
"ahash 0.7.6",
"anyhow",
@@ -3199,9 +3246,9 @@ dependencies = [
[[package]]
name = "serde-aux"
version = "4.1.2"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c599b3fd89a75e0c18d6d2be693ddb12cccaf771db4ff9e39097104808a014c0"
checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39"
dependencies = [
"chrono",
"serde",
@@ -3225,7 +3272,7 @@ checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -3291,7 +3338,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -3308,9 +3355,9 @@ dependencies = [
[[package]]
name = "serde_v8"
version = "0.88.0"
version = "0.90.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b163edac8a2f2536ae7b52f839a33aba7892c04d0cf21e1bf8213ecfc905ebcc"
checksum = "916ca7852a4c5f0ba59ce4a46301bf7c7ad573c2c89a0fe67e90fe30dcbd6f7d"
dependencies = [
"bytes",
"derive_more",
@@ -3323,9 +3370,9 @@ dependencies = [
[[package]]
name = "serde_yaml"
version = "0.9.19"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10"
checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c"
dependencies = [
"indexmap",
"itoa",
@@ -3689,6 +3736,28 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
]
[[package]]
name = "subtle"
version = "2.4.1"
@@ -3835,9 +3904,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.12"
version = "2.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927"
checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec"
dependencies = [
"proc-macro2",
"quote",
@@ -3900,7 +3969,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -4014,7 +4083,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.12",
"syn 2.0.13",
]
[[package]]
@@ -4309,7 +4378,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "typify"
version = "0.0.12-dev"
source = "git+https://github.com/oxidecomputer/typify#6d77f63b3dc5312cd36549507f941cb5d783600e"
source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4"
dependencies = [
"typify-impl",
"typify-macro",
@@ -4318,7 +4387,7 @@ dependencies = [
[[package]]
name = "typify-impl"
version = "0.0.12-dev"
source = "git+https://github.com/oxidecomputer/typify#6d77f63b3dc5312cd36549507f941cb5d783600e"
source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4"
dependencies = [
"heck",
"log",
@@ -4327,7 +4396,7 @@ dependencies = [
"regress",
"schemars",
"serde_json",
"syn 2.0.12",
"syn 2.0.13",
"thiserror",
"unicode-ident",
]
@@ -4335,7 +4404,7 @@ dependencies = [
[[package]]
name = "typify-macro"
version = "0.0.12-dev"
source = "git+https://github.com/oxidecomputer/typify#6d77f63b3dc5312cd36549507f941cb5d783600e"
source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4"
dependencies = [
"proc-macro2",
"quote",
@@ -4343,7 +4412,7 @@ dependencies = [
"serde",
"serde_json",
"serde_tokenstream",
"syn 2.0.12",
"syn 2.0.13",
"typify-impl",
]
@@ -4395,6 +4464,18 @@ dependencies = [
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-category"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0"
dependencies = [
"matches",
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-ident"
version = "0.9.0"
@@ -4491,9 +4572,9 @@ dependencies = [
[[package]]
name = "unsafe-libyaml"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c"
checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
[[package]]
name = "untrusted"
@@ -4531,7 +4612,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.9",
]
[[package]]
@@ -4540,15 +4621,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.9",
"serde",
]
[[package]]
name = "v8"
version = "0.66.0"
version = "0.68.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8ab8597b885c17b3761f6ffc29b7a62758612c409285a9271c6dacd17bb745"
checksum = "81c69410b7435f1b74e82e243ba906d71e8b9bb350828291418b9311dbd77222"
dependencies = [
"bitflags",
"fslock",
@@ -4780,7 +4861,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"axum",
@@ -4792,6 +4873,7 @@ dependencies = [
"rand 0.8.5",
"reqwest",
"rsa",
"serde",
"serde_json",
"sha2 0.10.6",
"sqlx",
@@ -4807,7 +4889,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"argon2",
@@ -4817,6 +4899,7 @@ dependencies = [
"axum",
"base64 0.21.0",
"chrono",
"chrono-tz",
"cookie",
"cron",
"futures",
@@ -4863,7 +4946,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"base64 0.21.0",
"chrono",
@@ -4878,7 +4961,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"chrono",
"serde",
@@ -4891,7 +4974,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"axum",
@@ -4916,7 +4999,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"serde",
"serde_json",
@@ -4924,7 +5007,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"itertools",
@@ -4939,9 +5022,10 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"gosyn",
"itertools",
"phf",
"unicode-general-category",
@@ -4951,7 +5035,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"itertools",
@@ -4966,7 +5050,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"deno_core",
@@ -4980,10 +5064,11 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"chrono",
"chrono-tz",
"cron",
"hex",
"hmac",
@@ -5003,7 +5088,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.84.0"
version = "1.86.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -5039,11 +5124,11 @@ dependencies = [
[[package]]
name = "windows"
version = "0.46.0"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets",
"windows-targets 0.48.0",
]
[[package]]
@@ -5052,13 +5137,13 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
@@ -5067,7 +5152,16 @@ version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.0",
]
[[package]]
@@ -5076,13 +5170,28 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
]
[[package]]
@@ -5091,42 +5200,84 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.1"
@@ -5190,9 +5341,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.7+zstd.1.5.4"
version = "2.0.8+zstd.1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5"
checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
dependencies = [
"cc",
"libc",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.84.0"
version = "1.86.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.84.0"
version = "1.86.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -28,11 +28,7 @@ name = "windmill"
path = "./src/main.rs"
[features]
enterprise = [
"windmill-worker/enterprise",
"windmill-queue/enterprise",
"windmill-api/enterprise",
]
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise"]
[dependencies]
anyhow.workspace = true
@@ -53,11 +49,13 @@ rsa.workspace = true
base64.workspace = true
sha2.workspace = true
[dev-dependencies]
serde_json.workspace = true
reqwest.workspace = true
windmill-queue.workspace = true
axum.workspace = true
serde.workspace = true
[workspace.dependencies]
windmill-api = { path = "./windmill-api" }
@@ -76,7 +74,7 @@ headers = "^0"
hyper = { version = "^0", features = ["full"] }
tokio = { version = "^1", features = ["full", "tracing"] }
tower = "^0"
tower-http = { version = "^0", features = ["trace"] }
tower-http = { version = "^0", features = ["trace", "cors"] }
tower-cookies = "^0"
serde = "^1"
serde_json = { version = "^1", features = ["preserve_order"] }
@@ -84,6 +82,7 @@ uuid = { version = "^1", features = ["serde", "v4"] }
thiserror = "^1"
anyhow = "^1"
chrono = { version = "^0", features = ["serde"] }
chrono-tz = "^0"
tracing = "^0"
tracing-subscriber = { version = "^0", features = ["env-filter", "json"] }
prometheus = { version = "^0", default-features = false }
@@ -153,4 +152,5 @@ async-stripe = { version = "0.14", features = [
"checkout",
] }
async_zip = { version = "0.0.11", features = ["full"] }
once_cell = "1.17.1"
once_cell = "1.17.1"
gosyn = "0.2.2"

View File

@@ -0,0 +1,2 @@
ALTER TABLE schedule DROP COLUMN timezone;
ALTER TABLE schedule ADD COLUMN offset_ INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,26 @@
ALTER TABLE schedule ADD COLUMN timezone VARCHAR(255) NOT NULL DEFAULT 'UTC';
-- INSERT the correct IANA timezone string for each offset value
UPDATE schedule SET timezone = 'Pacific/Honolulu' WHERE offset_ = 600;
UPDATE schedule SET timezone = 'America/Anchorage' WHERE offset_ = 540;
UPDATE schedule SET timezone = 'America/Los_Angeles' WHERE offset_ = 480;
UPDATE schedule SET timezone = 'America/Chicago' WHERE offset_ = 360;
UPDATE schedule SET timezone = 'America/New_York' WHERE offset_ = 300;
UPDATE schedule SET timezone = 'America/Halifax' WHERE offset_ = 240;
UPDATE schedule SET timezone = 'America/Sao_Paulo' WHERE offset_ = 180;
UPDATE schedule SET timezone = 'Atlantic/South_Georgia' WHERE offset_ = 120;
UPDATE schedule SET timezone = 'Atlantic/Cape_Verde' WHERE offset_ = 60;
UPDATE schedule SET timezone = 'Europe/London' WHERE offset_ = 0;
UPDATE schedule SET timezone = 'Europe/Berlin' WHERE offset_ = -60;
UPDATE schedule SET timezone = 'Europe/Athens' WHERE offset_ = -120;
UPDATE schedule SET timezone = 'Europe/Moscow' WHERE offset_ = -180;
UPDATE schedule SET timezone = 'Asia/Dubai' WHERE offset_ = -240;
UPDATE schedule SET timezone = 'Asia/Aqtau' WHERE offset_ = -300;
UPDATE schedule SET timezone = 'Asia/Almaty' WHERE offset_ = -360;
UPDATE schedule SET timezone = 'Asia/Bangkok' WHERE offset_ = -420;
UPDATE schedule SET timezone = 'Asia/Hong_Kong' WHERE offset_ = -480;
UPDATE schedule SET timezone = 'Asia/Tokyo' WHERE offset_ = -540;
UPDATE schedule SET timezone = 'Australia/Sydney' WHERE offset_ = -600;
ALTER TABLE schedule DROP COLUMN offset_;

View File

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

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE password ALTER COLUMN company TYPE VARCHAR(255);
ALTER TABLE password ALTER COLUMN name TYPE VARCHAR(255);

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE pip_resolution_cache;

View File

@@ -0,0 +1,6 @@
-- Add up migration script here
CREATE TABLE pip_resolution_cache(
hash VARCHAR(255) PRIMARY KEY,
expiration TIMESTAMP NOT NULL,
lockfile TEXT NOT NULL
);

View File

@@ -15,3 +15,4 @@ phf.workspace = true
unicode-general-category.workspace = true
itertools.workspace = true
anyhow.workspace = true
gosyn.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -1,347 +0,0 @@
#![allow(clippy::large_enum_variant)] // TODO: we allow large enum variant for now, let's profile properly to see if we want to box.
use crate::parser_go_token::{Position, Token};
use std::collections::BTreeMap;
// https://pkg.go.dev/go/ast#CommentGroup
#[derive(Debug)]
pub struct CommentGroup {
// List []*Comment // len(List) > 0
}
// https://pkg.go.dev/go/ast#FieldList
#[derive(Debug)]
pub struct FieldList<'a> {
pub opening: Option<Position<'a>>, // position of opening parenthesis/brace, if any
pub list: Vec<Field<'a>>, // field list; or nil
pub closing: Option<Position<'a>>, // position of closing parenthesis/brace, if any
}
// https://pkg.go.dev/go/ast#Field
#[derive(Debug)]
pub struct Field<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub names: Option<Vec<Ident<'a>>>, // field/method/(type) parameter names, or type "type"; or nil
pub type_: Option<Expr<'a>>, // field/method/parameter type, type list type; or nil
pub tag: Option<BasicLit<'a>>, // field tag; or nil
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#File
#[derive(Debug)]
pub struct File<'a> {
// package name
pub decls: Vec<Decl<'a>>, // top-level declarations; or nil // list of all comments in the source file
}
// https://pkg.go.dev/go/ast#FuncDecl
#[derive(Debug)]
pub struct FuncDecl<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub recv: Option<FieldList<'a>>, // receiver (methods); or nil (functions)
pub name: Ident<'a>, // function/method name
pub type_: FuncType<'a>, // function signature: type and value parameters, results, and position of "func" keyword
pub body: Option<BlockStmt<'a>>, // function body; or nil for external (non-Go) function
}
// https://pkg.go.dev/go/ast#BlockStmt
#[derive(Debug)]
pub struct BlockStmt<'a> {
pub lbrace: Position<'a>, // position of "{"
pub list: Vec<Stmt>,
pub rbrace: Position<'a>, // position of "}", if any (may be absent due to syntax error)
}
// https://pkg.go.dev/go/ast#FuncType
#[derive(Debug)]
pub struct FuncType<'a> {
pub func: Option<Position<'a>>, // position of "func" keyword (token.NoPos if there is no "func")
pub params: FieldList<'a>, // (incoming) parameters; non-nil
pub results: Option<FieldList<'a>>, // (outgoing) results; or nil
}
// https://pkg.go.dev/go/ast#Ident
#[derive(Debug)]
pub struct Ident<'a> {
pub name_pos: Position<'a>, // identifier position
pub name: &'a str, // identifier name
pub obj: Option<Box<Object<'a>>>, // denoted object; or nil
}
// https://pkg.go.dev/go/ast#ValueSpec
#[derive(Debug)]
pub struct ValueSpec<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub names: Vec<Ident<'a>>, // value names (len(Names) > 0)
pub type_: Option<Expr<'a>>, // value type; or nil
pub values: Option<Vec<Expr<'a>>>, // initial values; or nil
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#BasicLit
#[derive(Debug)]
pub struct BasicLit<'a> {
pub value_pos: Position<'a>, // literal position
pub kind: Token, // token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING
pub value: &'a str, // literal string; e.g. 42, 0x7f, 3.14, 1e-9, 2.4i, 'a', '\x7f', "foo" or `\m\n\o`
}
// https://pkg.go.dev/go/ast#Object
#[derive(Debug)]
pub struct Object<'a> {
pub kind: ObjKind,
pub name: &'a str, // declared name
pub decl: Option<ObjDecl>, // corresponding Field, XxxSpec, FuncDecl, LabeledStmt, AssignStmt, Scope; or nil
pub data: Option<usize>, // object-specific data; or nil
pub type_: Option<()>, // placeholder for type information; may be nil
}
// https://pkg.go.dev/go/ast#Ellipsis
#[derive(Debug)]
pub struct Ellipsis<'a> {
pub ellipsis: Position<'a>, // position of "..."
pub elt: Option<Box<Expr<'a>>>, // ellipsis element type (parameter lists only); or nil
}
// https://pkg.go.dev/go/ast#Ellipsis
#[derive(Debug)]
pub struct TypeAssertExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lparen: Position<'a>, // position of "("
pub type_: Box<Expr<'a>>, // asserted type; nil means type switch X.(type)
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#SliceExpr
#[derive(Debug)]
pub struct SliceExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lbrack: Position<'a>, // position of "["
pub low: Option<Box<Expr<'a>>>, // begin of slice range; or nil
pub high: Option<Box<Expr<'a>>>, // end of slice range; or nil
pub max: Option<Box<Expr<'a>>>, // maximum capacity of slice; or nil
pub slice3: bool, // true if 3-index slice (2 colons present)
pub rbrack: Position<'a>, // position of "]"
}
// https://pkg.go.dev/go/ast#ObjKind
#[derive(Debug)]
pub enum ObjKind {}
#[derive(Debug)]
pub enum ObjDecl {}
// https://pkg.go.dev/go/ast#Decl
#[derive(Debug)]
pub enum Decl<'a> {
FuncDecl(FuncDecl<'a>),
}
// https://pkg.go.dev/go/ast#Scope
#[derive(Debug)]
pub struct Scope<'a> {
pub outer: Option<Box<Scope<'a>>>,
pub objects: BTreeMap<&'a str, Object<'a>>,
}
// https://pkg.go.dev/go/ast#GenDecl
#[derive(Debug)]
pub struct GenDecl<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub tok_pos: Position<'a>, // position of Tok
pub tok: Token, // IMPORT, CONST, TYPE, or VAR
pub lparen: Option<Position<'a>>, // position of '(', if any
pub specs: Vec<Spec>,
pub rparen: Option<Position<'a>>, // position of ')', if any
}
// https://pkg.go.dev/go/ast#AssignStmt
#[derive(Debug)]
pub struct AssignStmt<'a> {
pub lhs: Vec<Expr<'a>>,
pub tok_pos: Position<'a>, // position of Tok
pub tok: Token, // assignment token, DEFINE
pub rhs: Vec<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#BinaryExpr
#[derive(Debug)]
pub struct BinaryExpr<'a> {
pub x: Box<Expr<'a>>, // left operand
pub op_pos: Position<'a>, // position of Op
pub op: Token, // operator
pub y: Box<Expr<'a>>, // right operand
}
// https://pkg.go.dev/go/ast#ReturnStmt
#[derive(Debug)]
pub struct ReturnStmt<'a> {
pub return_: Position<'a>, // position of "return" keyword
pub results: Vec<Expr<'a>>, // result expressions; or nil
}
// https://pkg.go.dev/go/ast#TypeSpec
#[derive(Debug)]
pub struct TypeSpec<'a> {
pub doc: Option<CommentGroup>, // associated documentation; or nil
pub name: Option<Ident<'a>>, // type name
pub assign: Option<Position<'a>>, // position of '=', if any
pub type_: Expr<'a>, // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
pub comment: Option<CommentGroup>, // line comments; or nil
}
// https://pkg.go.dev/go/ast#StructType
#[derive(Debug)]
pub struct StructType<'a> {
pub struct_: Position<'a>, // position of "struct" keyword
pub fields: Option<FieldList<'a>>, // list of field declarations
pub incomplete: bool, // true if (source) fields are missing in the Fields list
}
// https://pkg.go.dev/go/ast#StarExpr
#[derive(Debug)]
pub struct StarExpr<'a> {
pub star: Position<'a>, // position of "*"
pub x: Box<Expr<'a>>, // operand
}
// https://pkg.go.dev/go/ast#InterfaceType
#[derive(Debug)]
pub struct InterfaceType<'a> {
pub interface: Position<'a>, // position of "interface" keyword
pub methods: Option<FieldList<'a>>, // list of embedded interfaces, methods, or types
pub incomplete: bool, // true if (source) methods or types are missing in the Methods list
}
// https://pkg.go.dev/go/ast#UnaryExpr
#[derive(Debug)]
pub struct UnaryExpr<'a> {
pub op_pos: Position<'a>, // position of Op
pub op: Token, // operator
pub x: Box<Expr<'a>>, // operand
}
// https://pkg.go.dev/go/ast#CallExpr
#[derive(Debug)]
pub struct CallExpr<'a> {
pub fun: Box<Expr<'a>>, // function expression
pub lparen: Position<'a>, // position of "("
pub args: Option<Vec<Expr<'a>>>, // function arguments; or nil
pub ellipsis: Option<Position<'a>>, // position of "..." (token.NoPos if there is no "...")
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#SelectorExpr
#[derive(Debug)]
pub struct SelectorExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub sel: Ident<'a>, // field selector
}
// https://pkg.go.dev/go/ast#ParenExpr
#[derive(Debug)]
pub struct ParenExpr<'a> {
pub lparen: Position<'a>, // position of "("
pub x: Box<Expr<'a>>, // parenthesized expression
pub rparen: Position<'a>, // position of ")"
}
// https://pkg.go.dev/go/ast#FuncLit
#[derive(Debug)]
pub struct FuncLit<'a> {
pub type_: FuncType<'a>, // function type
pub body: BlockStmt<'a>, // function body
}
// https://pkg.go.dev/go/ast#ChanType
#[derive(Debug)]
pub struct ChanType<'a> {
pub begin: Position<'a>, // position of "chan" keyword or "<-" (whichever comes first)
pub arrow: Option<Position<'a>>, // position of "<-" (token.NoPos if there is no "<-")
pub dir: u8, // channel direction
pub value: Box<Expr<'a>>, // value type
}
// htt/opt/visual-studio-code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.htmlps://pkg.go.dev/go/ast#IndexExpr
#[derive(Debug)]
pub struct IndexExpr<'a> {
pub x: Box<Expr<'a>>, // expression
pub lbrack: Position<'a>, // position of "["
pub index: Box<Expr<'a>>, // index expression
pub rbrack: Position<'a>, // position of "]"
}
// https://pkg.go.dev/go/ast#MapType
#[derive(Debug)]
pub struct MapType<'a> {
pub map: Position<'a>,
pub key: Box<Expr<'a>>,
pub value: Box<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#CompositeLit
#[derive(Debug)]
pub struct CompositeLit<'a> {
pub type_: Box<Expr<'a>>, // literal type; or nil
pub lbrace: Position<'a>, // position of "{"
pub elts: Option<Vec<Expr<'a>>>, // list of composite elements; or nil
pub rbrace: Position<'a>, // position of "}"
pub incomplete: bool, // true if (source) expressions are missing in the Elts list
}
// https://pkg.go.dev/go/ast#KeyValueExpr
#[derive(Debug)]
pub struct KeyValueExpr<'a> {
pub key: Box<Expr<'a>>,
pub colon: Position<'a>, // position of ":"
pub value: Box<Expr<'a>>,
}
// https://pkg.go.dev/go/ast#ArrayType
#[derive(Debug)]
pub struct ArrayType<'a> {
pub lbrack: Position<'a>, // position of "["
pub len: Option<Box<Expr<'a>>>, // Ellipsis node for [...]T array types, nil for slice types
pub elt: Box<Expr<'a>>, // element type
}
// https://pkg.go.dev/go/ast#ChanDir
#[derive(Debug)]
pub enum ChanDir {
SEND = 1 << 0,
RECV = 1 << 1,
}
// https://pkg.go.dev/go/ast#Spec
#[derive(Debug)]
pub enum Spec {}
// https://pkg.go.dev/go/ast#Expr
#[derive(Debug)]
pub enum Expr<'a> {
ArrayType(ArrayType<'a>),
BasicLit(BasicLit<'a>),
BinaryExpr(BinaryExpr<'a>),
CallExpr(CallExpr<'a>),
ChanType(ChanType<'a>),
CompositeLit(CompositeLit<'a>),
Ellipsis(Ellipsis<'a>),
FuncLit(FuncLit<'a>),
FuncType(FuncType<'a>),
Ident(Ident<'a>),
IndexExpr(IndexExpr<'a>),
InterfaceType(InterfaceType<'a>),
KeyValueExpr(KeyValueExpr<'a>),
MapType(MapType<'a>),
ParenExpr(ParenExpr<'a>),
SelectorExpr(SelectorExpr<'a>),
SliceExpr(SliceExpr<'a>),
StarExpr(StarExpr<'a>),
StructType(StructType<'a>),
TypeAssertExpr(TypeAssertExpr<'a>),
UnaryExpr(UnaryExpr<'a>),
}
// https://pkg.go.dev/go/ast#Stmt
#[derive(Debug)]
pub enum Stmt {}

View File

@@ -1,948 +0,0 @@
// https://golang.org/ref/spec#Lexical_elements
use crate::parser_go_token::{Position, Token};
use phf::{phf_map, Map};
use std::fmt;
use unicode_general_category::{get_general_category, GeneralCategory};
pub type Step<'a> = (Position<'a>, Token, &'a str);
#[derive(Debug)]
pub enum ScannerError {
HexadecimalNotFound,
OctalNotFound,
UnterminatedComment,
UnterminatedEscapedChar,
UnterminatedRune,
UnterminatedString,
InvalidDirective,
}
impl std::error::Error for ScannerError {}
impl fmt::Display for ScannerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "scanner error: {:?}", self)
}
}
pub type Result<T> = std::result::Result<T, ScannerError>;
#[derive(Debug)]
pub struct Scanner<'a> {
directory: &'a str,
file: &'a str,
buffer: &'a str,
//
chars: std::iter::Peekable<std::str::Chars<'a>>,
current_char: Option<char>,
current_char_len: usize,
//
offset: usize,
line: usize,
column: usize,
start_offset: usize,
start_line: usize,
start_column: usize,
//
hide_column: bool,
insert_semi: bool,
pending_line_info: Option<LineInfo<'a>>,
}
type LineInfo<'a> = (Option<&'a str>, usize, Option<usize>, bool);
impl<'a> Scanner<'a> {
pub fn new(filename: &'a str, buffer: &'a str) -> Self {
let (directory, file) = filename.rsplit_once('/').unwrap_or(("", filename));
let mut s = Scanner {
directory,
file,
buffer,
//
chars: buffer.chars().peekable(),
current_char: None,
current_char_len: 0,
//
offset: 0,
line: 1,
column: 1,
start_offset: 0,
start_line: 1,
start_column: 1,
//
hide_column: false,
insert_semi: false,
pending_line_info: None,
};
s.next(); // read the first character
s
}
#[allow(clippy::cognitive_complexity)] // Allow complex scan function
pub fn scan(&mut self) -> Result<Step<'a>> {
let insert_semi = self.insert_semi;
self.insert_semi = false;
while let Some(c) = self.current_char {
self.reset_start();
match c {
' ' | '\t' | '\r' => {
self.next();
}
'\n' => {
self.next();
if insert_semi {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
}
_ => break,
}
}
if let Some(c) = self.current_char {
match c {
'+' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::ADD_ASSIGN, ""));
}
Some('+') => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::INC, ""));
}
_ => return Ok((self.position(), Token::ADD, "")),
}
}
'-' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SUB_ASSIGN, ""));
}
Some('-') => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::DEC, ""));
}
_ => return Ok((self.position(), Token::SUB, "")),
}
}
'*' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::MUL_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::MUL, "")),
}
}
'/' => match self.peek() {
Some('=') => {
self.next();
self.next();
return Ok((self.position(), Token::QUO_ASSIGN, ""));
}
Some('/') => {
if insert_semi {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
return self.scan_line_comment();
}
Some('*') => {
if insert_semi && self.find_line_end() {
return Ok((self.position(), Token::SEMICOLON, "\n"));
}
return self.scan_general_comment();
}
_ => {
self.next();
return Ok((self.position(), Token::QUO, ""));
}
},
'%' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::REM_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::REM, "")),
}
}
'&' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::AND_ASSIGN, ""));
}
Some('&') => {
self.next();
return Ok((self.position(), Token::LAND, ""));
}
Some('^') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::AND_NOT_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::AND_NOT, "")),
}
}
_ => return Ok((self.position(), Token::AND, "")),
}
}
'|' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::OR_ASSIGN, ""));
}
Some('|') => {
self.next();
return Ok((self.position(), Token::LOR, ""));
}
_ => return Ok((self.position(), Token::OR, "")),
}
}
'^' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::XOR_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::XOR, "")),
}
}
'<' => {
self.next();
match self.current_char {
Some('<') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SHL_ASSIGN, ""));
}
_ => return Ok((self.position(), Token::SHL, "")),
}
}
Some('=') => {
self.next();
return Ok((self.position(), Token::LEQ, ""));
}
Some('-') => {
self.next();
return Ok((self.position(), Token::ARROW, ""));
}
_ => return Ok((self.position(), Token::LSS, "")),
}
}
'>' => {
self.next();
match self.current_char {
Some('>') => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::SHR_ASSIGN, ""));
}
_ => {
return Ok((self.position(), Token::SHR, ""));
}
}
}
Some('=') => {
self.next();
return Ok((self.position(), Token::GEQ, ""));
}
_ => return Ok((self.position(), Token::GTR, "")),
}
}
':' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::DEFINE, ""));
}
_ => return Ok((self.position(), Token::COLON, "")),
}
}
'!' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::NEQ, ""));
}
_ => return Ok((self.position(), Token::NOT, "")),
}
}
',' => {
self.next();
return Ok((self.position(), Token::COMMA, ""));
}
'(' => {
self.next();
return Ok((self.position(), Token::LPAREN, ""));
}
')' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RPAREN, ""));
}
'[' => {
self.next();
return Ok((self.position(), Token::LBRACK, ""));
}
']' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RBRACK, ""));
}
'{' => {
self.next();
return Ok((self.position(), Token::LBRACE, ""));
}
'}' => {
self.insert_semi = true;
self.next();
return Ok((self.position(), Token::RBRACE, ""));
}
';' => {
self.next();
return Ok((self.position(), Token::SEMICOLON, ";"));
}
'.' => {
self.next();
match self.current_char {
Some('0'..='9') => return self.scan_int_or_float_or_imag(true),
Some('.') => match self.peek() {
Some('.') => {
self.next();
self.next();
return Ok((self.position(), Token::ELLIPSIS, ""));
}
_ => return Ok((self.position(), Token::PERIOD, "")),
},
_ => return Ok((self.position(), Token::PERIOD, "")),
}
}
'=' => {
self.next();
match self.current_char {
Some('=') => {
self.next();
return Ok((self.position(), Token::EQL, ""));
}
_ => return Ok((self.position(), Token::ASSIGN, "")),
}
}
'0'..='9' => return self.scan_int_or_float_or_imag(false),
'\'' => return self.scan_rune(),
'"' => return self.scan_interpreted_string(),
'`' => return self.scan_raw_string(),
_ => return self.scan_pkg_or_keyword_or_ident(),
};
}
self.reset_start();
if insert_semi {
Ok((self.position(), Token::SEMICOLON, "\n"))
} else {
Ok((self.position(), Token::EOF, ""))
}
}
// https://golang.org/ref/spec#Keywords
// https://golang.org/ref/spec#Identifiers
fn scan_pkg_or_keyword_or_ident(&mut self) -> Result<Step<'a>> {
self.next();
while let Some(c) = self.current_char {
if !(is_letter(c) || is_unicode_digit(c)) {
break;
}
self.next()
}
let pos = self.position();
let literal = self.literal();
if literal.len() > 1 {
if let Some(&token) = KEYWORDS.get(literal) {
self.insert_semi = matches!(
token,
Token::BREAK | Token::CONTINUE | Token::FALLTHROUGH | Token::RETURN
);
return Ok((pos, token, literal));
}
}
self.insert_semi = true;
Ok((pos, Token::IDENT, literal))
}
// https://golang.org/ref/spec#Integer_literals
// https://golang.org/ref/spec#Floating-point_literals
// https://golang.org/ref/spec#Imaginary_literals
fn scan_int_or_float_or_imag(&mut self, preceding_dot: bool) -> Result<Step<'a>> {
self.insert_semi = true;
let mut token = Token::INT;
let mut digits = "_0123456789";
let mut exp = "eE";
if !preceding_dot {
if matches!(self.current_char, Some('0')) {
self.next();
match self.current_char {
Some('b' | 'B') => {
digits = "_01";
exp = "";
self.next();
}
Some('o' | 'O') => {
digits = "_01234567";
exp = "";
self.next();
}
Some('x' | 'X') => {
digits = "_0123456789abcdefABCDEF";
exp = "pP";
self.next();
}
_ => {}
};
}
while let Some(c) = self.current_char {
if !digits.contains(c) {
break;
}
self.next();
}
}
if preceding_dot || matches!(self.current_char, Some('.')) {
token = Token::FLOAT;
self.next();
while let Some(c) = self.current_char {
if !digits.contains(c) {
break;
}
self.next();
}
}
if !exp.is_empty() {
if let Some(c) = self.current_char {
if exp.contains(c) {
token = Token::FLOAT;
self.next();
if matches!(self.current_char, Some('-' | '+')) {
self.next();
}
while let Some(c) = self.current_char {
if !matches!(c, '_' | '0'..='9') {
break;
}
self.next();
}
}
}
}
if matches!(self.current_char, Some('i')) {
token = Token::IMAG;
self.next();
}
Ok((self.position(), token, self.literal()))
}
// https://golang.org/ref/spec#Rune_literals
fn scan_rune(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
match self.current_char {
Some('\\') => self.require_escaped_char::<'\''>()?,
Some(_) => self.next(),
_ => return Err(ScannerError::UnterminatedRune),
}
if matches!(self.current_char, Some('\'')) {
self.next();
return Ok((self.position(), Token::CHAR, self.literal()));
}
Err(ScannerError::UnterminatedRune)
}
// https://golang.org/ref/spec#String_literals
fn scan_interpreted_string(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
while let Some(c) = self.current_char {
match c {
'"' => {
self.next();
return Ok((self.position(), Token::STRING, self.literal()));
}
'\\' => self.require_escaped_char::<'"'>()?,
_ => self.next(),
}
}
Err(ScannerError::UnterminatedString)
}
// https://golang.org/ref/spec#String_literals
fn scan_raw_string(&mut self) -> Result<Step<'a>> {
self.insert_semi = true;
self.next();
while let Some(c) = self.current_char {
match c {
'`' => {
self.next();
return Ok((self.position(), Token::STRING, self.literal()));
}
_ => self.next(),
}
}
Err(ScannerError::UnterminatedString)
}
// https://golang.org/ref/spec#Comments
fn scan_general_comment(&mut self) -> Result<Step<'a>> {
self.next();
self.next();
while let Some(c) = self.current_char {
match c {
'*' => {
self.next();
if matches!(self.current_char, Some('/')) {
self.next();
let pos = self.position();
let lit = self.literal();
// look for compiler directives
self.directive(&lit["/*".len()..lit.len() - "*/".len()], true)?;
return Ok((pos, Token::COMMENT, lit));
}
}
_ => self.next(),
}
}
Err(ScannerError::UnterminatedComment)
}
// https://golang.org/ref/spec#Comments
fn scan_line_comment(&mut self) -> Result<Step<'a>> {
self.next();
self.next();
while let Some(c) = self.current_char {
if is_newline(c) {
break;
}
self.next();
}
let pos = self.position();
let lit = self.literal();
// look for compiler directives (at the beginning of line)
if self.start_column == 1 {
self.directive(lit["//".len()..].trim_end(), false)?;
}
Ok((pos, Token::COMMENT, self.literal()))
}
// https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives
fn directive(&mut self, input: &'a str, immediate: bool) -> Result<()> {
if let Some(line_directive) = input.strip_prefix("line ") {
self.pending_line_info = self.parse_line_directive(line_directive)?;
if immediate {
self.consume_pending_line_info();
}
}
Ok(())
}
fn parse_line_directive(&mut self, line_directive: &'a str) -> Result<Option<LineInfo<'a>>> {
if let Some((file, line)) = line_directive.rsplit_once(':') {
let line = line.parse().map_err(|_| ScannerError::InvalidDirective)?;
if let Some((file, l)) = file.rsplit_once(':') {
if let Ok(l) = l.parse() {
//line :line:col
//line filename:line:col
/*line :line:col*/
/*line filename:line:col*/
let file = if !file.is_empty() { Some(file) } else { None };
let col = Some(line);
let line = l;
let hide_column = false;
return Ok(Some((file, line, col, hide_column)));
}
}
//line :line
//line filename:line
/*line :line*/
/*line filename:line*/
Ok(Some((Some(file), line, None, true)))
} else {
Ok(None)
}
}
const fn find_line_end(&self) -> bool {
let buffer = self.buffer.as_bytes();
let mut in_comment = true;
let mut i = self.offset;
let max = self.buffer.len();
while i < max {
let c = buffer[i] as char;
if i < max - 1 {
let n = buffer[i + 1] as char;
if !in_comment && c == '/' && n == '/' {
return true;
}
if c == '/' && n == '*' {
i += 2;
in_comment = true;
continue;
}
if c == '*' && n == '/' {
i += 2;
in_comment = false;
continue;
}
}
if is_newline(c) {
return true;
}
if !in_comment && !matches!(c, ' ' | '\t' | '\r') {
return false;
}
i += 1;
}
!in_comment
}
fn consume_pending_line_info(&mut self) {
if let Some(line_info) = self.pending_line_info.take() {
if let Some(file) = line_info.0 {
self.file = file;
}
self.line = line_info.1;
if let Some(column) = line_info.2 {
self.column = column;
}
self.hide_column = line_info.3;
}
}
fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
fn next(&mut self) {
self.offset += self.current_char_len;
self.column += self.current_char_len;
let last_char = self.current_char;
self.current_char = self.chars.next();
if let Some(c) = self.current_char {
self.current_char_len = c.len_utf8();
if matches!(last_char, Some('\n')) {
self.line += 1;
self.column = 1;
self.consume_pending_line_info();
}
} else {
self.current_char_len = 0
}
}
const fn position(&self) -> Position<'a> {
Position {
directory: self.directory,
file: self.file,
offset: self.start_offset,
line: self.start_line,
column: if self.hide_column {
0
} else {
self.start_column
},
}
}
fn reset_start(&mut self) {
self.start_offset = self.offset;
self.start_line = self.line;
self.start_column = self.column;
}
fn literal(&self) -> &'a str {
&self.buffer[self.start_offset..self.offset]
}
fn require_escaped_char<const DELIM: char>(&mut self) -> Result<()> {
self.next();
let c = self
.current_char
.ok_or(ScannerError::UnterminatedEscapedChar)?;
// TODO: move this to the match when const generics can be referenced in patterns
if c == DELIM {
self.next();
return Ok(());
}
match c {
'a' | 'b' | 'f' | 'n' | 'r' | 't' | 'v' | '\\' => self.next(),
'x' => {
self.next();
self.require_hex_digits::<2>()?
}
'u' => {
self.next();
self.require_hex_digits::<4>()?;
}
'U' => {
self.next();
self.require_hex_digits::<8>()?;
}
'0'..='7' => self.require_octal_digits::<3>()?,
_ => return Err(ScannerError::UnterminatedEscapedChar),
}
Ok(())
}
fn require_octal_digits<const COUNT: usize>(&mut self) -> Result<()> {
for _ in 0..COUNT {
let c = self.current_char.ok_or(ScannerError::OctalNotFound)?;
if !is_octal_digit(c) {
return Err(ScannerError::OctalNotFound);
}
self.next();
}
Ok(())
}
fn require_hex_digits<const COUNT: usize>(&mut self) -> Result<()> {
for _ in 0..COUNT {
let c = self.current_char.ok_or(ScannerError::HexadecimalNotFound)?;
if !is_hex_digit(c) {
return Err(ScannerError::HexadecimalNotFound);
}
self.next();
}
Ok(())
}
}
impl<'a> IntoIterator for Scanner<'a> {
type Item = Result<Step<'a>>;
type IntoIter = IntoIter<'a>;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter::new(self)
}
}
pub struct IntoIter<'a> {
scanner: Scanner<'a>,
done: bool,
}
impl<'a> IntoIter<'a> {
const fn new(scanner: Scanner<'a>) -> Self {
Self { scanner, done: false }
}
}
impl<'a> Iterator for IntoIter<'a> {
type Item = Result<Step<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
match self.scanner.scan() {
Ok((pos, tok, lit)) => {
if tok == Token::EOF {
self.done = true;
}
Some(Ok((pos, tok, lit)))
}
Err(err) => {
self.done = true;
Some(Err(err))
}
}
}
}
// https://golang.org/ref/spec#Letters_and_digits
fn is_letter(c: char) -> bool {
c == '_' || is_unicode_letter(c)
}
//const fn is_decimal_digit(c: char) -> bool {
//matches!(c, '0'..='9')
//}
//const fn is_binary_digit(c: char) -> bool {
//matches!(c, '0'..='1')
//}
const fn is_octal_digit(c: char) -> bool {
matches!(c, '0'..='7')
}
const fn is_hex_digit(c: char) -> bool {
matches!(c, '0'..='9' | 'A'..='F' | 'a'..='f')
}
// https://golang.org/ref/spec#Characters
const fn is_newline(c: char) -> bool {
c == '\n'
}
//const fn is_unicode_char(c: char) -> bool {
//c != '\n'
//}
fn is_unicode_letter(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
)
}
fn is_unicode_digit(c: char) -> bool {
get_general_category(c) == GeneralCategory::DecimalNumber
}
// https://golang.org/ref/spec#Keywords
static KEYWORDS: Map<&'static str, Token> = phf_map! {
"break" => Token::BREAK,
"case" => Token::CASE,
"chan" => Token::CHAN,
"const" => Token::CONST,
"continue" => Token::CONTINUE,
"default" => Token::DEFAULT,
"defer" => Token::DEFER,
"else" => Token::ELSE,
"fallthrough" => Token::FALLTHROUGH,
"for" => Token::FOR,
"func" => Token::FUNC,
"go" => Token::GO,
"goto" => Token::GOTO,
"if" => Token::IF,
"import" => Token::IMPORT,
"interface" => Token::INTERFACE,
"map" => Token::MAP,
"package" => Token::PACKAGE,
"range" => Token::RANGE,
"return" => Token::RETURN,
"select" => Token::SELECT,
"struct" => Token::STRUCT,
"switch" => Token::SWITCH,
"type" => Token::TYPE,
"var" => Token::VAR,
};
#[cfg(test)]
mod tests {
use super::Scanner;
#[test] // fuzz
fn it_should_return_an_error_on_missing_line_number() {
let input = "/*line :*/";
let mut out: Vec<_> = Scanner::new(file!(), input).into_iter().collect();
assert!(out.pop().unwrap().is_err());
}
}

View File

@@ -1,273 +0,0 @@
// https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/go/token/token.go
#![allow(non_camel_case_types)] // For consistency with the Go tokens
use std::fmt;
#[derive(Clone, Copy, Debug, Default)]
pub struct Position<'a> {
pub directory: &'a str,
pub file: &'a str,
pub offset: usize,
pub line: usize,
pub column: usize,
}
impl<'a> fmt::Display for Position<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.file.is_empty() {
write!(f, ":{}:{}", self.line, self.column)
} else if self.file.starts_with('/') {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
} else {
write!(
f,
"{}/{}:{}:{}",
self.directory, self.file, self.line, self.column
)
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Token {
EOF,
COMMENT,
IDENT, // main
INT, // 12345
FLOAT, // 123.45
IMAG, // 123.45i
CHAR, // 'a'
STRING, // "abc"
ADD, // +
SUB, // -
MUL, // *
QUO, // /
REM, // %
AND, // &
OR, // |
XOR, // ^
SHL, // <<
SHR, // >>
AND_NOT, // &^
ADD_ASSIGN, // +=
SUB_ASSIGN, // -=
MUL_ASSIGN, // *=
QUO_ASSIGN, // /=
REM_ASSIGN, // %=
AND_ASSIGN, // &=
OR_ASSIGN, // |=
XOR_ASSIGN, // ^=
SHL_ASSIGN, // <<=
SHR_ASSIGN, // >>=
AND_NOT_ASSIGN, // &^=
LAND, // &&
LOR, // ||
ARROW, // <-
INC, // ++
DEC, // --
EQL, // ==
LSS, // <
GTR, // >
ASSIGN, // =
NOT, // !
NEQ, // !=
LEQ, // <=
GEQ, // >=
DEFINE, // :=
ELLIPSIS, // ...
LPAREN, // (
LBRACK, // [
LBRACE, // {
COMMA, // ,
PERIOD, // .
RPAREN, // )
RBRACK, // ]
RBRACE, // }
SEMICOLON, // ;
COLON, // :
BREAK,
CASE,
CHAN,
CONST,
CONTINUE,
DEFAULT,
DEFER,
ELSE,
FALLTHROUGH,
FOR,
FUNC,
GO,
GOTO,
IF,
IMPORT,
INTERFACE,
MAP,
PACKAGE,
RANGE,
RETURN,
SELECT,
STRUCT,
SWITCH,
TYPE,
VAR,
}
impl Token {
pub const fn is_assign_op(&self) -> bool {
use Token::*;
matches!(
self,
ADD_ASSIGN
| SUB_ASSIGN
| MUL_ASSIGN
| QUO_ASSIGN
| REM_ASSIGN
| AND_ASSIGN
| OR_ASSIGN
| XOR_ASSIGN
| SHL_ASSIGN
| SHR_ASSIGN
| AND_NOT_ASSIGN
)
}
// https://go.dev/ref/spec#Operator_precedence
pub fn precedence(&self) -> u8 {
use Token::*;
match self {
MUL | QUO | REM | SHL | SHR | AND | AND_NOT => 5,
ADD | SUB | OR | XOR => 4,
EQL | NEQ | LSS | LEQ | GTR | GEQ => 3,
LAND => 2,
LOR => 1,
_ => unreachable!(
"precedence() is only supported for binary operators, called with: {:?}",
self
),
}
}
pub const fn lowest_precedence() -> u8 {
0
}
}
impl From<&Token> for &'static str {
fn from(token: &Token) -> Self {
use Token::*;
match token {
EOF => "EOF",
COMMENT => "COMMENT",
IDENT => "IDENT",
INT => "INT",
FLOAT => "FLOAT",
IMAG => "IMAG",
CHAR => "CHAR",
STRING => "STRING",
ADD => "+",
SUB => "-",
MUL => "*",
QUO => "/",
REM => "%",
AND => "&",
OR => "|",
XOR => "^",
SHL => "<<",
SHR => ">>",
AND_NOT => "&^",
ADD_ASSIGN => "+=",
SUB_ASSIGN => "-=",
MUL_ASSIGN => "*=",
QUO_ASSIGN => "/=",
REM_ASSIGN => "%=",
AND_ASSIGN => "&=",
OR_ASSIGN => "|=",
XOR_ASSIGN => "^=",
SHL_ASSIGN => "<<=",
SHR_ASSIGN => ">>=",
AND_NOT_ASSIGN => "&^=",
LAND => "&&",
LOR => "||",
ARROW => "<-",
INC => "++",
DEC => "--",
EQL => "==",
LSS => "<",
GTR => ">",
ASSIGN => "=",
NOT => "!",
NEQ => "!=",
LEQ => "<=",
GEQ => ">=",
DEFINE => ":=",
ELLIPSIS => "...",
LPAREN => "(",
LBRACK => "[",
LBRACE => "{",
COMMA => ",",
PERIOD => ".",
RPAREN => ")",
RBRACK => "]",
RBRACE => "}",
SEMICOLON => ";",
COLON => ":",
BREAK => "break",
CASE => "case",
CHAN => "chan",
CONST => "const",
CONTINUE => "continue",
DEFAULT => "default",
DEFER => "defer",
ELSE => "else",
FALLTHROUGH => "fallthrough",
FOR => "for",
FUNC => "func",
GO => "go",
GOTO => "goto",
IF => "if",
IMPORT => "import",
INTERFACE => "interface",
MAP => "map",
PACKAGE => "package",
RANGE => "range",
RETURN => "return",
SELECT => "select",
STRUCT => "struct",
SWITCH => "switch",
TYPE => "type",
VAR => "var",
}
}
}

View File

@@ -218,7 +218,7 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
let ast = parser::parse_program(code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code: {}", e.to_string()))
})?;
let imports = ast
let mut imports: Vec<String> = ast
.into_iter()
.filter_map(|x| match x {
Located { node, .. } => match node {
@@ -250,7 +250,7 @@ pub fn parse_python_imports(code: &str) -> error::Result<Vec<String>> {
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
.unique()
.collect();
imports.sort();
Ok(imports)
}
}
@@ -460,7 +460,7 @@ def main():
";
let r = parse_python_imports(code)?;
// println!("{}", serde_json::to_string(&r)?);
assert_eq!(r, vec!["wmill", "zanzibar", "matplotlib", "requests"]);
assert_eq!(r, vec!["matplotlib", "requests", "wmill", "zanzibar"]);
Ok(())
}

View File

@@ -6,21 +6,22 @@
* LICENSE-AGPL for a copy of the license.
*/
use deno_core::{serde_v8, v8, JsRuntime, RuntimeOptions};
use serde_json::Value;
use windmill_common::error;
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Spanned};
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
use swc_ecma_ast::{
ArrayLit, AssignPat, BigInt, BindingIdent, Bool, Decl, ExportDecl, Expr, FnDecl, Ident, Lit,
ModuleDecl, ModuleItem, Number, ObjectLit, Pat, Str, TsArrayType, TsEntityName, TsKeywordType,
TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature, TsType,
TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
ModuleDecl, ModuleItem, Number, ObjectLit, Param, Pat, Str, TsArrayType, TsEntityName,
TsKeywordType, TsKeywordTypeKind, TsLit, TsLitType, TsOptionalType, TsPropertySignature,
TsType, TsTypeElement, TsTypeLit, TsTypeRef, TsUnionOrIntersectionType, TsUnionType,
};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
pub fn parse_deno_signature(code: &str, skip_dflt: bool) -> error::Result<MainArgSignature> {
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Custom("test.ts".into()), code.into());
let fm = cm.new_source_file(FileName::Custom("main.ts".into()), code.into());
let lexer = Lexer::new(
// We want to parse ecmascript
Syntax::Typescript(TsConfig::default()),
@@ -54,65 +55,17 @@ pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
})) if &sym.to_string() == "main" => Some(function.params),
_ => None,
});
if let Some(params) = params {
Ok(MainArgSignature {
let r = MainArgSignature {
star_args: false,
star_kwargs: false,
args: params
.into_iter()
.map(|x| match x.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: ident.id.optional || nullable,
})
}
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(left.span())
.unwrap_or_else(|_| cm.span_to_string(left.span()))
))
})?;
let span = match *right {
Expr::Lit(Lit::Str(Str { span, .. })) => Some(span),
Expr::Lit(Lit::Num(Number { span, .. })) => Some(span),
Expr::Lit(Lit::BigInt(BigInt { span, .. })) => Some(span),
Expr::Lit(Lit::Bool(Bool { span, .. })) => Some(span),
Expr::Object(ObjectLit { span, .. }) => Some(span),
Expr::Array(ArrayLit { span, .. }) => Some(span),
_ => None,
};
let expr = span
.and_then(|x| cm.span_to_snippet(x).ok())
.map(|x| serde_json::from_str(&x).map_err(|_| x));
let default = match expr.clone() {
Some(Ok(x)) => Some(x),
Some(Err(x)) => eval_sync(&x).ok(),
None => None,
};
if typ == Typ::Unknown && default.is_some() {
typ = json_to_typ(default.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default, has_default: true })
}
_ => Err(error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span()))
))),
})
.map(|x| parse_param(x, &cm, skip_dflt))
.collect::<Result<Vec<Arg>, error::Error>>()?,
})
};
Ok(r)
} else {
Err(error::Error::ExecutionErr(
"main function was not findable (expected to find 'export function main(...)'"
@@ -121,6 +74,70 @@ pub fn parse_deno_signature(code: &str) -> error::Result<MainArgSignature> {
}
}
fn parse_param(x: Param, cm: &Lrc<SourceMap>, skip_dflt: bool) -> error::Result<Arg> {
let r = match x.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(&ident);
Ok(Arg {
otyp: None,
name,
typ,
default: None,
has_default: ident.id.optional || nullable,
})
}
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) =
left.as_ident().map(binding_ident_to_arg).ok_or_else(|| {
error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(left.span())
.unwrap_or_else(|_| cm.span_to_string(left.span()))
))
})?;
let dflt = if skip_dflt {
None
} else {
match *right {
Expr::Lit(Lit::Str(Str { value, .. })) => {
Some(Value::String(value.to_string()))
}
Expr::Lit(Lit::Num(Number { value, .. })) => Some(serde_json::json!(value)),
Expr::Lit(Lit::BigInt(BigInt { value, .. })) => Some(serde_json::json!(value)),
Expr::Lit(Lit::Bool(Bool { value, .. })) => Some(Value::Bool(value)),
Expr::Object(ObjectLit { span, .. }) => eval_span(span, cm),
Expr::Array(ArrayLit { span, .. }) => eval_span(span, cm),
_ => None,
}
};
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap());
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true })
}
_ => Err(error::Error::ExecutionErr(format!(
"parameter syntax unsupported: `{}`",
cm.span_to_snippet(x.span())
.unwrap_or_else(|_| cm.span_to_string(x.span()))
))),
};
r
}
fn eval_span(span: Span, cm: &Lrc<SourceMap>) -> Option<Value> {
let expr = cm
.span_to_snippet(span)
.ok()
.map(|x| serde_json::from_str(&x).map_err(|_| x));
match expr {
Some(Ok(x)) => Some(x),
Some(Err(x)) => eval_sync(&x).ok(),
None => None,
}
}
fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String, Typ, bool) {
let (typ, nullable) = type_ann
.as_ref()
@@ -282,7 +299,7 @@ export function main(test1?: string, test2: string = \"burkina\",
}
";
assert_eq!(
parse_deno_signature(code)?,
parse_deno_signature(code, false)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
@@ -394,7 +411,7 @@ export function main(test2 = \"burkina\",
}
";
assert_eq!(
parse_deno_signature(code)?,
parse_deno_signature(code, false)?,
MainArgSignature {
star_args: false,
star_kwargs: false,

View File

@@ -1319,6 +1319,24 @@
},
"query": "SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE name = $1 AND workspace_id = $2"
},
"399a8337a2488fa2ce3da2ef3281a34f8f96ee0d833c2fe3c22a0aa43e306f09": {
"describe": {
"columns": [
{
"name": "hash",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": []
}
},
"query": "DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash"
},
"39bae7dff750565183ac3893b51a8846d7db2f130a6795b54ef94acc232dec8b": {
"describe": {
"columns": [
@@ -1790,6 +1808,19 @@
},
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)\n WHERE id = $2\n "
},
"4fb3881cdbb4b9e93e28f460a9b3715bdc6a52b76c89f3a3913023b13c4e085c": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "INSERT INTO pip_resolution_cache (hash, lockfile, expiration) VALUES ($1, $2, now() + ('3 days')::interval) ON CONFLICT (hash) DO UPDATE SET lockfile = $2"
},
"5061c0d054bf4f028e7fe51a8f9389024c6ae4492755cadac0f7167e5300bda0": {
"describe": {
"columns": [],
@@ -1993,101 +2024,6 @@
},
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5)"
},
"55960eb3cd5a82f593496349a2521607f8066d88ff11a2ca4a63b6e37e62b71b": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true
],
"parameters": {
"Left": [
"Varchar",
"Jsonb",
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET schedule = $1, args = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *"
},
"56839d3aec6c0177d14589aedda8d5c431d841b6d5d0d99ce3836bb42d4d83d9": {
"describe": {
"columns": [
@@ -2602,6 +2538,102 @@
},
"query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8"
},
"7226d470c04e58fcab92f3edac21956d2a10e5ed6b594747b22682f05a60a341": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "enabled",
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Jsonb",
"Text",
"Text"
]
}
},
"query": "UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path = $4 AND workspace_id = $5 RETURNING *"
},
"73d3ed17fd0723ba75722f394904f6ee306b59aaf8ecfcf56484d38541343f06": {
"describe": {
"columns": [],
@@ -3007,6 +3039,26 @@
},
"query": "SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3"
},
"866b26e56fd376368c759db6eee13f92373e308f584903121b4546ad51aef86e": {
"describe": {
"columns": [
{
"name": "lockfile",
"ordinal": 0,
"type_info": "Text"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT lockfile FROM pip_resolution_cache WHERE hash = $1"
},
"8876fa929ffb175cd976a2bca1195704aa9fe7215013ae29e49ef15cb201ba57": {
"describe": {
"columns": [],
@@ -3195,45 +3247,45 @@
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
@@ -3244,12 +3296,12 @@
false,
false,
false,
false,
true,
false,
false,
false,
true
true,
false
],
"parameters": {
"Left": [
@@ -3472,45 +3524,45 @@
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
@@ -3521,12 +3573,12 @@
false,
false,
false,
false,
true,
false,
false,
false,
true
true,
false
],
"parameters": {
"Left": [
@@ -3659,45 +3711,45 @@
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
@@ -3708,12 +3760,12 @@
false,
false,
false,
false,
true,
false,
false,
false,
true
true,
false
],
"parameters": {
"Left": [
@@ -4237,45 +4289,45 @@
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
@@ -4286,12 +4338,12 @@
false,
false,
false,
false,
true,
false,
false,
false,
true
true,
false
],
"parameters": {
"Left": [
@@ -4812,45 +4864,45 @@
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
@@ -4861,12 +4913,12 @@
false,
false,
false,
false,
true,
false,
false,
false,
true
true,
false
],
"parameters": {
"Left": [
@@ -5690,107 +5742,6 @@
},
"query": "\n UPDATE queue\n SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'iterator', 'index'], ((flow_status->'modules'->$1::int->'iterator'->>'index')::int + 1)::text::jsonb)\n WHERE id = $2\n RETURNING (flow_status->'modules'->$1::int->'iterator'->>'index')::int\n "
},
"e5ba82116000d2ae71e2099f417b39121178349fda9f8438bdd5556ff632ceb2": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Int4",
"Varchar",
"Varchar",
"Bool",
"Jsonb",
"Bool",
"Varchar"
]
}
},
"query": "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *"
},
"e6f85cdbe681ace495fde31e67339dc58460b6914440473fe94de7a7bc292af4": {
"describe": {
"columns": [],
@@ -5885,6 +5836,107 @@
},
"query": "INSERT INTO workspace_settings\n (workspace_id, slack_team_id, slack_name, slack_email)\n VALUES ($1, $2, $3, $4) ON CONFLICT (workspace_id) DO UPDATE SET slack_team_id = $2, slack_name = $3, slack_email = $4"
},
"ebbabe9b71804ee983235db7d7844052139c23ae7a27cae925c19e1c01037143": {
"describe": {
"columns": [
{
"name": "workspace_id",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "path",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "edited_by",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "edited_at",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "schedule",
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "enabled",
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
false,
false,
false,
true,
false
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Jsonb",
"Bool",
"Varchar"
]
}
},
"query": "INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *"
},
"ebc06efe51532f7f60e98f62d09a3453fc1d97d93e96ec849eac96dca05236f0": {
"describe": {
"columns": [
@@ -5963,45 +6015,45 @@
"ordinal": 4,
"type_info": "Varchar"
},
{
"name": "offset_",
"ordinal": 5,
"type_info": "Int4"
},
{
"name": "enabled",
"ordinal": 6,
"ordinal": 5,
"type_info": "Bool"
},
{
"name": "script_path",
"ordinal": 7,
"ordinal": 6,
"type_info": "Varchar"
},
{
"name": "args",
"ordinal": 8,
"ordinal": 7,
"type_info": "Jsonb"
},
{
"name": "extra_perms",
"ordinal": 9,
"ordinal": 8,
"type_info": "Jsonb"
},
{
"name": "is_flow",
"ordinal": 10,
"ordinal": 9,
"type_info": "Bool"
},
{
"name": "email",
"ordinal": 11,
"ordinal": 10,
"type_info": "Varchar"
},
{
"name": "error",
"ordinal": 12,
"ordinal": 11,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 12,
"type_info": "Varchar"
}
],
"nullable": [
@@ -6012,12 +6064,12 @@
false,
false,
false,
false,
true,
false,
false,
false,
true
true,
false
],
"parameters": {
"Left": [

View File

@@ -1,4 +1,6 @@
#[cfg(feature = "enterprise")]
use base64::Engine;
#[cfg(feature = "enterprise")]
use rsa::{pkcs8::DecodePublicKey, signature::Verifier};
#[cfg(feature = "enterprise")]
use sha2::Sha256;
@@ -11,9 +13,9 @@ pub fn verify_license_key(license_key: Option<String>) -> anyhow::Result<()> {
.expect("license_key can be splitted with a .");
let pub_key = rsa::RsaPublicKey::from_public_key_der(
&base64::decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
let msg = base64::decode(splitted_lk.0)?;
let signature = base64::decode(splitted_lk.1)?;
&base64::engine::general_purpose::STANDARD.decode("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgVShzcLSPiOi+8ET8fggob1kmi47/cE12JaidPkwfGnScZItghkqtiLsct0U4kJhlp5gO89DYTBmIKadvxwY7kMsLlZzmi2emVH7c27cByGASY8QmWDNdG4Ggy/NDflGGBdAtN6gHawZAg4zHv3qpbPQGHH1/6sXIohcXhOnouwIDAQAB")?)?;
let msg = base64::engine::general_purpose::STANDARD.decode(splitted_lk.0)?;
let signature = base64::engine::general_purpose::STANDARD.decode(splitted_lk.1)?;
rsa::pss::VerifyingKey::<Sha256>::new(pub_key)
.verify(&msg, &rsa::pss::Signature::from(signature))
.map_err(|_| anyhow::anyhow!("Invalid license key".to_string()))?;

View File

@@ -10,7 +10,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use git_version::git_version;
use sqlx::{Pool, Postgres};
use windmill_common::utils::rd_string;
use windmill_common::{utils::rd_string, METRICS_ADDR};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
const DEFAULT_NUM_WORKERS: usize = 3;
@@ -30,15 +30,7 @@ async fn main() -> anyhow::Result<()> {
.and_then(|x| x.parse::<i32>().ok())
.unwrap_or(DEFAULT_NUM_WORKERS as i32);
let metrics_addr: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose()?
.flatten();
let metrics_addr: Option<SocketAddr> = *METRICS_ADDR;
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
.ok()
@@ -156,9 +148,11 @@ Windmill Community Edition {GIT_VERSION}
let metrics_f = async {
match metrics_addr {
Some(addr) => windmill_common::serve_metrics(addr, rx.resubscribe())
.await
.map_err(anyhow::Error::from),
Some(addr) => {
windmill_common::serve_metrics(addr, rx.resubscribe(), num_workers > 0)
.await
.map_err(anyhow::Error::from)
}
None => Ok(()),
}
};

View File

@@ -1,4 +1,5 @@
use futures::{stream, Stream};
use serde::Deserialize;
use serde_json::json;
use sqlx::{postgres::PgListener, types::Uuid, Pool, Postgres, Transaction};
use windmill_api::jobs::{CompletedJob, Job};
@@ -252,9 +253,7 @@ mod suspend_resume {
let second = completed.next().await.unwrap();
// print_job(second, &db).await;
let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
tx.commit().await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}&approver=ruben"
))
@@ -355,9 +354,7 @@ mod suspend_resume {
/* ... and send a request resume it. */
let second = completed.next().await.unwrap();
let tx = db.begin().await.unwrap();
let (tx, token) = windmill_worker::create_token_for_owner(tx, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
tx.commit().await.unwrap();
let token = windmill_worker::create_token_for_owner(&db, "test-workspace", "u/test-user", "", 100, "").await.unwrap();
let secret = reqwest::get(format!(
"http://localhost:{port}/api/w/test-workspace/jobs/job_signature/{second}/0?token={token}"
))
@@ -1518,6 +1515,8 @@ async fn test_go_job(db: Pool<Postgres>) {
let port = server.addr.port();
let content = r#"
package inner
import "fmt"
func main(derp string) (string, error) {
@@ -2024,6 +2023,16 @@ async fn test_branchall_simple(db: Pool<Postgres>) {
assert_eq!(result, serde_json::json!([[1, 2], [1, 3]]));
}
#[derive(Deserialize)]
struct ErrorResult {
error: NamedError,
}
#[derive(Deserialize)]
struct NamedError {
name: String,
}
#[sqlx::test(fixtures("base"))]
async fn test_branchall_skip_failure(db: Pool<Postgres>) {
initialize_tracing().await;
@@ -2059,8 +2068,11 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
.unwrap();
assert_eq!(
result,
serde_json::json!([{"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/main.ts:1:31)\n at run (file:///tmp/wrapper.ts:9:26)\n at file:///tmp/wrapper.ts:14:1", "message": "failure"}}, [1,3]])
serde_json::from_value::<ErrorResult>(result)
.unwrap()
.error
.name,
"Error"
);
let flow: FlowValue = serde_json::from_value(json!({
@@ -2093,8 +2105,11 @@ async fn test_branchall_skip_failure(db: Pool<Postgres>) {
.unwrap();
assert_eq!(
result,
serde_json::json!([ {"error": {"name": "Error", "stack": "Error: failure\n at main (file:///tmp/main.ts:1:31)\n at run (file:///tmp/wrapper.ts:9:26)\n at file:///tmp/wrapper.ts:14:1", "message": "failure"}}, [1, 2]])
serde_json::from_value::<ErrorResult>(result)
.unwrap()
.error
.name,
"Error"
);
}

View File

@@ -47,6 +47,7 @@ tracing.workspace = true
sql-builder.workspace = true
serde_json.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true
base64.workspace = true
serde_urlencoded.workspace = true

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.84.0
version: 1.86.0
title: Windmill API
contact:
@@ -1048,6 +1048,27 @@ paths:
schema:
type: string
/users/tokens/impersonate:
post:
summary: create token to impersonate a user (require superadmin)
operationId: createTokenImpersonate
tags:
- user
requestBody:
description: new token
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewTokenImpersonate"
responses:
"201":
description: token created
content:
text/plain:
schema:
type: string
/users/tokens/delete/{token_prefix}:
delete:
summary: delete token
@@ -2021,8 +2042,7 @@ paths:
schema:
type: string
- name: first_parent_hash
description:
mask to filter scripts whom first direct parent has exact hash
description: mask to filter scripts whom first direct parent has exact hash
in: query
schema:
type: string
@@ -2279,8 +2299,7 @@ paths:
/w/{workspace}/scripts/delete/h/{hash}:
post:
summary:
delete script by hash (erase content but keep hash, require admin)
summary: delete script by hash (erase content but keep hash, require admin)
operationId: deleteScriptByHash
tags:
- script
@@ -2346,6 +2365,24 @@ paths:
schema:
type: string
/scripts_u/tokened_raw/{workspace}/{token}/{path}:
get:
summary: raw script by path with a token (mostly used by lsp to be used with import maps to resolve scripts)
operationId: rawScriptByPathTokened
tags:
- script
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Token"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: script content
content:
text/plain:
schema:
type: string
/w/{workspace}/scripts/exists/p/{path}:
get:
summary: exists script by path
@@ -2435,15 +2472,13 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -2473,18 +2508,6 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
@@ -2504,6 +2527,26 @@ paths:
application/json:
schema: {}
get:
summary: run script by path with get
operationId: runWaitResultScriptByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/run_wait_result/f/{path}:
post:
summary: run flow by path and wait until completion
@@ -2513,18 +2556,6 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
@@ -3023,16 +3054,14 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the flow owner (default false)
description: make the run invisible to the the flow owner (default false)
in: query
schema:
type: boolean
@@ -3070,16 +3099,14 @@ paths:
type: string
format: date-time
- name: scheduled_in_secs
description:
schedule the script to execute in the number of seconds starting now
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3110,8 +3137,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3142,8 +3168,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description:
make the run invisible to the the script owner (default false)
description: make the run invisible to the the script owner (default false)
in: query
schema:
type: boolean
@@ -3483,8 +3508,7 @@ paths:
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
get:
summary:
get resume urls given a job_id, resume_id and a nonce to resume a flow
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
operationId: getResumeUrls
tags:
- job
@@ -3528,6 +3552,7 @@ paths:
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/JobId"
- $ref: "#/components/parameters/Payload"
- name: resume_id
in: path
required: true
@@ -3741,14 +3766,14 @@ paths:
properties:
schedule:
type: string
offset:
type: integer
timezone:
type: string
required:
- schedule
- timezone
responses:
"200":
description:
the preview of the next 10 time this schedule would apply to
description: List of 5 estimated upcoming execution events (in UTC)
content:
application/json:
schema:
@@ -3935,8 +3960,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description:
only list the groups the user is member of (default false)
description: only list the groups the user is member of (default false)
schema:
type: boolean
responses:
@@ -4124,8 +4148,7 @@ paths:
- $ref: "#/components/parameters/WorkspaceId"
- name: only_member_of
in: query
description:
only list the folders the user is member of (default false)
description: only list the folders the user is member of (default false)
schema:
type: boolean
responses:
@@ -4551,6 +4574,12 @@ components:
required: true
schema:
type: string
Token:
name: token
in: path
required: true
schema:
type: string
AccountId:
name: id
in: path
@@ -4608,8 +4637,7 @@ components:
type: integer
PerPage:
name: per_page
description:
number of items to return for a given page (default 30, max 100)
description: number of items to return for a given page (default 30, max 100)
in: query
schema:
type: integer
@@ -4649,6 +4677,14 @@ components:
in: query
schema:
type: string
Payload:
name: payload
description: |
The base64 encoded payload that has been encoded as a JSON. e.g how to encode such payload encodeURIComponent
`encodeURIComponent(btoa(JSON.stringify({a: 2})))`
in: query
schema:
type: string
ScriptStartPath:
name: script_path_start
@@ -4702,16 +4738,14 @@ components:
type: boolean
ArgsFilter:
name: args
description:
filter on jobs containing those args as a json subset (@> in postgres)
description: filter on jobs containing those args as a json subset (@> in postgres)
in: query
schema:
type: string
ResultFilter:
name: result
description:
filter on jobs containing those result as a json subset (@> in postgres)
description: filter on jobs containing those result as a json subset (@> in postgres)
in: query
schema:
type: string
@@ -5143,6 +5177,19 @@ components:
type: string
format: date-time
NewTokenImpersonate:
type: object
properties:
label:
type: string
expiration:
type: string
format: date-time
impersonate_email:
type: string
required:
- impersonate_email
ListableVariable:
type: object
properties:
@@ -5268,6 +5315,11 @@ components:
MainArgSignature:
type: object
properties:
type:
type: string
enum: ["Valid", "Invalid"]
error:
type: string
star_args:
type: boolean
star_kwargs:
@@ -5377,6 +5429,8 @@ components:
- star_args
- start_kwargs
- args
- type
- error
Preview:
type: object
@@ -5511,8 +5565,8 @@ components:
format: date-time
schedule:
type: string
offset_:
type: integer
timezone:
type: string
enabled:
type: boolean
script_path:
@@ -5535,7 +5589,7 @@ components:
- edited_at
- schedule
- script_path
- offset_
- timezone
- extra_perms
- is_flow
- enabled
@@ -5548,8 +5602,8 @@ components:
type: string
schedule:
type: string
offset:
type: integer
timezone:
type: string
script_path:
type: string
is_flow:
@@ -5561,6 +5615,7 @@ components:
required:
- path
- schedule
- timezone
- script_path
- is_flow
- args
@@ -5570,10 +5625,13 @@ components:
properties:
schedule:
type: string
timezone:
type: string
args:
$ref: "#/components/schemas/ScriptArgs"
required:
- schedule
- timezone
- script_path
- is_flow
- args

View File

@@ -30,7 +30,7 @@ use windmill_queue::{push, schedule::push_scheduled_job, JobPayload};
use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -178,12 +178,15 @@ async fn check_path_conflict<'c>(
async fn create_flow(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(nf): Json<NewFlow>,
) -> Result<(StatusCode, String)> {
// cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?;
let authed = maybe_refresh_folders(&nf.path, &w_id, authed, &db).await;
let mut tx = user_db.clone().begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &nf.path).await?;
@@ -288,9 +291,11 @@ async fn update_flow(
Path((w_id, flow_path)): Path<(String, StripPath)>,
Json(nf): Json<NewFlow>,
) -> Result<String> {
let flow_path = flow_path.to_path();
let authed = maybe_refresh_folders(&flow_path, &w_id, authed, &db).await;
let mut tx = user_db.clone().begin(&authed).await?;
let flow_path = flow_path.to_path();
check_schedule_conflict(&mut tx, &w_id, flow_path).await?;
let schema = nf.schema.map(|x| x.0);
@@ -625,7 +630,6 @@ mod tests {
"modules": [
{
"id": "a",
"input_transforms": {},
"value": {
"input_transforms": {
"test": {
@@ -639,7 +643,6 @@ mod tests {
},
{
"id": "b",
"input_transforms": {},
"value": {
"input_transforms": {},
"type": "rawscript",
@@ -653,7 +656,6 @@ mod tests {
},
{
"id": "c",
"input_transforms": {},
"value": {
"type": "forloopflow",
"iterator": {
@@ -676,7 +678,6 @@ mod tests {
],
"failure_module": {
"id": "d",
"input_transforms": {},
"value": {
"input_transforms": {},
"type": "script",

View File

@@ -6,9 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::sync::Arc;
use crate::{
db::{UserDB, DB},
users::Authed,
users::{AuthCache, Authed, Tokened},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -144,8 +146,10 @@ lazy_static! {
async fn create_folder(
authed: Authed,
Tokened { token }: Tokened,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(cache): Extension<Arc<AuthCache>>,
Path(w_id): Path<String>,
Json(ng): Json<NewFolder>,
) -> Result<String> {
@@ -156,8 +160,8 @@ async fn create_folder(
"Folder name can only contain alphanumeric characters, underscores"
)));
}
check_name_conflict(&mut tx, &w_id, &ng.name).await?;
cache.invalidate(&w_id, token).await;
let owner = username_to_permissioned_as(&authed.username);
let owners = &ng.owners.unwrap_or(vec![owner.clone()]);

View File

@@ -47,6 +47,10 @@ pub fn workspaced_service() -> Router {
"/run_wait_result/p/*script_path",
post(run_wait_result_job_by_path),
)
.route(
"/run_wait_result/p/*script_path",
get(run_wait_result_job_by_path_get),
)
.route(
"/run_wait_result/h/:hash",
post(run_wait_result_job_by_hash),
@@ -309,6 +313,7 @@ pub struct RunJobQuery {
include_header: Option<String>,
invisible_to_owner: Option<bool>,
queue_limit: Option<i64>,
payload: Option<String>,
}
lazy_static::lazy_static! {
@@ -1194,7 +1199,7 @@ where
}
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
fn decode_payload<D: DeserializeOwned>(t: String) -> anyhow::Result<D> {
let vec = base64::engine::general_purpose::URL_SAFE
.decode(t)
.context("invalid base64")?;
@@ -1385,6 +1390,60 @@ lazy_static::lazy_static! {
.and_then(|x| x.parse().ok())
.unwrap_or(20);
}
pub async fn run_wait_result_job_by_path_get(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, script_path)): Path<(String, StripPath)>,
Query(run_query): Query<RunJobQuery>,
) -> error::JsonResult<serde_json::Value> {
let payload_r = run_query
.payload
.map(decode_payload)
.map(|x| x.map_err(|e| Error::InternalErr(e.to_string())));
let args = if let Some(payload) = payload_r {
payload?
} else {
serde_json::Map::new()
};
check_queue_too_long(db, QUEUE_LIMIT_WAIT_RESULT.or(run_query.queue_limit)).await?;
let script_path = script_path.to_path();
let mut tx = user_db.clone().begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let (uuid, tx) = push(
tx,
&w_id,
job_payload,
args,
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
None,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
)
.await?;
tx.commit().await?;
run_wait_result(
authed,
Extension(user_db),
*TIMEOUT_WAIT_RESULT,
uuid,
Path((w_id, script_path)),
)
.await
}
pub async fn run_wait_result_job_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -1398,7 +1457,6 @@ pub async fn run_wait_result_job_by_path(
let script_path = script_path.to_path();
let mut tx = user_db.clone().begin(&authed).await?;
let job_payload = script_path_to_payload(script_path, &mut tx, &w_id).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
@@ -1410,7 +1468,7 @@ pub async fn run_wait_result_job_by_path(
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
run_query.parent_job,
run_query.parent_job,
@@ -1446,7 +1504,6 @@ pub async fn run_wait_result_job_by_hash(
let hash = script_hash.0;
let mut tx = user_db.clone().begin(&authed).await?;
let path = get_path_for_hash(&mut tx, &w_id, hash).await?;
let scheduled_for = run_query.get_scheduled_for(&mut tx).await?;
let args = run_query.add_include_headers(headers, args.unwrap_or_default());
let (uuid, tx) = push(
@@ -1457,7 +1514,7 @@ pub async fn run_wait_result_job_by_hash(
&authed.username,
&authed.email,
username_to_permissioned_as(&authed.username),
scheduled_for,
None,
None,
run_query.parent_job,
run_query.parent_job,

View File

@@ -11,11 +11,15 @@ use argon2::Argon2;
use axum::{middleware::from_extractor, routing::get, Extension, Router};
use db::DB;
use git_version::git_version;
use hyper::Method;
use reqwest::Client;
use std::{net::SocketAddr, sync::Arc};
use tower::ServiceBuilder;
use tower_cookies::CookieManagerLayer;
use tower_http::trace::TraceLayer;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use windmill_common::utils::rd_string;
use crate::{
@@ -100,6 +104,11 @@ pub async fn run_server(
.layer(Extension(auth_cache.clone()))
.layer(CookieManagerLayer::new())
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())));
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_origin(Any);
// build our application with a route
let app = Router::new()
.nest(
@@ -109,7 +118,7 @@ pub async fn run_server(
"/w/:workspace_id",
Router::new()
.nest("/scripts", scripts::workspaced_service())
.nest("/jobs", jobs::workspaced_service())
.nest("/jobs", jobs::workspaced_service().layer(cors.clone()))
.nest(
"/users",
users::workspaced_service().layer(Extension(argon2.clone())),
@@ -136,16 +145,25 @@ pub async fn run_server(
.nest("/workers", worker_ping::global_service())
.nest("/scripts", scripts::global_service())
.nest("/flows", flows::global_service())
.nest("/apps", apps::global_service())
.nest("/apps", apps::global_service().layer(cors.clone()))
.nest("/schedules", schedule::global_service())
.route_layer(from_extractor::<Authed>())
.route_layer(from_extractor::<users::Tokened>())
.nest("/scripts_u", scripts::global_unauthed_service())
.nest(
"/w/:workspace_id/apps_u",
apps::unauthed_service().layer(from_extractor::<OptAuthed>()),
apps::unauthed_service()
.layer(from_extractor::<OptAuthed>())
.layer(cors.clone()),
)
.nest(
"/w/:workspace_id/jobs_u",
jobs::global_service().layer(cors.clone()),
)
.nest(
"/w/:workspace_id/capture_u",
capture::global_service().layer(cors),
)
.nest("/w/:workspace_id/jobs_u", jobs::global_service())
.nest("/w/:workspace_id/capture_u", capture::global_service())
.nest(
"/auth",
users::make_unauthed_service().layer(Extension(argon2)),

View File

@@ -849,7 +849,8 @@ async fn login_callback(
_ => user.email.ok_or_else(|| {
error::Error::BadRequest("email address not fetchable from user info".to_string())
})?,
};
}
.to_lowercase();
if let Some(domains) = &client_w_config.allowed_domains {
if !domains.iter().any(|d| email.ends_with(d)) {

View File

@@ -8,7 +8,7 @@
use crate::{
db::{UserDB, DB},
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
use axum::{
@@ -264,9 +264,12 @@ async fn create_resource(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(resource): Json<CreateResource>,
) -> Result<(StatusCode, String)> {
let authed = maybe_refresh_folders(&resource.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &resource.path).await?;
@@ -375,6 +378,7 @@ async fn update_resource(
}
sqlb.returning("path");
let authed = maybe_refresh_folders(path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;

View File

@@ -6,20 +6,19 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::str::FromStr;
use crate::{
db::{UserDB, DB},
users::Authed,
users::{maybe_refresh_folders, Authed},
};
use axum::{
extract::{Extension, Path, Query},
routing::{delete, get, post},
Json, Router,
};
use chrono::{DateTime, FixedOffset};
use chrono::{DateTime, Utc};
use serde::Deserialize;
use sqlx::{Postgres, Transaction};
use std::str::FromStr;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -47,7 +46,7 @@ pub fn global_service() -> Router {
pub struct NewSchedule {
pub path: String,
pub schedule: String,
pub offset: i32,
pub timezone: String,
pub script_path: String,
pub is_flow: bool,
pub args: Option<serde_json::Value>,
@@ -78,10 +77,13 @@ async fn check_path_conflict<'c>(
async fn create_schedule(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(ns): Json<NewSchedule>,
) -> Result<String> {
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
cron::Schedule::from_str(&ns.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
check_path_conflict(&mut tx, &w_id, &ns.path).await?;
@@ -89,12 +91,12 @@ async fn create_schedule(
let schedule = sqlx::query_as!(
Schedule,
"INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, \
"INSERT INTO schedule (workspace_id, path, schedule, timezone, edited_by, script_path, \
is_flow, args, enabled, email) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *",
w_id,
ns.path,
ns.schedule,
ns.offset,
ns.timezone,
&authed.username,
ns.script_path,
ns.is_flow,
@@ -135,13 +137,17 @@ async fn create_schedule(
async fn edit_schedule(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
Json(es): Json<EditSchedule>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
cron::Schedule::from_str(&es.schedule).map_err(|e| Error::BadRequest(e.to_string()))?;
let is_flow = sqlx::query_scalar!(
@@ -155,9 +161,10 @@ async fn edit_schedule(
clear_schedule(&mut tx, path, is_flow).await?;
let schedule = sqlx::query_as!(
Schedule,
"UPDATE schedule SET schedule = $1, args = $2 WHERE path \
= $3 AND workspace_id = $4 RETURNING *",
"UPDATE schedule SET schedule = $1, timezone = $2, args = $3 WHERE path \
= $4 AND workspace_id = $5 RETURNING *",
es.schedule,
es.timezone,
es.args,
path,
w_id,
@@ -235,15 +242,26 @@ async fn exists_schedule(
Ok(Json(res))
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub timezone: String,
}
pub async fn preview_schedule(
Json(payload): Json<PreviewPayload>,
) -> JsonResult<Vec<DateTime<chrono::Utc>>> {
) -> JsonResult<Vec<DateTime<Utc>>> {
let schedule = cron::Schedule::from_str(&payload.schedule)
.map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<chrono::Utc>> = schedule
.upcoming(get_offset(payload.offset))
.take(10)
.map(|x| x.into())
let tz =
chrono_tz::Tz::from_str(&payload.timezone).map_err(|e| Error::BadRequest(e.to_string()))?;
let upcoming: Vec<DateTime<Utc>> = schedule
.upcoming(tz)
.take(5)
// Convert back to UTC for a standardised API response. The client will convert to the local timezone.
.map(|x| x.with_timezone(&Utc))
.collect();
Ok(Json(upcoming))
@@ -353,6 +371,7 @@ async fn check_flow_conflict<'c>(
#[derive(Deserialize)]
pub struct EditSchedule {
pub schedule: String,
pub timezone: String,
pub args: Option<serde_json::Value>,
}
@@ -376,16 +395,6 @@ pub async fn clear_schedule<'c>(
Ok(())
}
#[derive(Deserialize)]
pub struct PreviewPayload {
pub schedule: String,
pub offset: Option<i32>,
}
fn get_offset(offset: Option<i32>) -> FixedOffset {
FixedOffset::west_opt(offset.unwrap_or(0) * 60).expect("Invalid offset")
}
#[derive(Deserialize)]
pub struct SetEnabled {
pub enabled: bool,

View File

@@ -8,11 +8,12 @@
use sql_builder::prelude::*;
use windmill_audit::{audit_log, ActionKind};
use windmill_parser::MainArgSignature;
use crate::{
db::{UserDB, DB},
schedule::clear_schedule,
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, AuthCache, Authed},
webhook_util::{WebhookMessage, WebhookShared},
HTTP_CLIENT,
};
@@ -29,6 +30,7 @@ use sqlx::{FromRow, Postgres, Transaction};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::Arc,
};
use windmill_common::{
error::{Error, JsonResult, Result},
@@ -60,6 +62,13 @@ pub fn global_service() -> Router {
.route("/hub/get_full/*path", get(get_full_hub_script_by_path))
}
pub fn global_unauthed_service() -> Router {
Router::new().route(
"/tokened_raw/:workspace/:token/*path",
get(get_tokened_raw_script_by_path),
)
}
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_scripts))
@@ -190,6 +199,7 @@ async fn create_script(
Json(ns): Json<NewScript>,
) -> Result<(StatusCode, String)> {
let hash = ScriptHash(hash_script(&ns));
let authed = maybe_refresh_folders(&ns.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
if sqlx::query_scalar!(
@@ -496,6 +506,18 @@ async fn list_paths(
Ok(Json(scripts))
}
async fn get_tokened_raw_script_by_path(
Extension(user_db): Extension<UserDB>,
Path((w_id, token, path)): Path<(String, String, StripPath)>,
Extension(cache): Extension<Arc<AuthCache>>,
) -> Result<String> {
let authed = cache
.get_authed(Some(w_id.clone()), &token)
.await
.ok_or_else(|| Error::NotAuthorized("Invalid token".to_string()))?;
return raw_script_by_path(authed, Extension(user_db), Path((w_id, path))).await;
}
async fn raw_script_by_path(
authed: Authed,
Extension(user_db): Extension<UserDB>,
@@ -556,11 +578,10 @@ async fn get_script_by_hash_internal<'c>(
}
async fn get_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<Script> {
let mut tx = user_db.begin(&authed).await?;
let mut tx = db.begin().await?;
let r = get_script_by_hash_internal(&mut tx, &w_id, &hash).await?;
tx.commit().await?;
@@ -568,11 +589,10 @@ async fn get_script_by_hash(
}
async fn raw_script_by_hash(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, hash_str)): Path<(String, String)>,
) -> Result<String> {
let mut tx = user_db.begin(&authed).await?;
let mut tx = db.begin().await?;
let hash = ScriptHash(to_i64(hash_str.strip_suffix(".ts").ok_or_else(|| {
Error::BadRequest("Raw script path must end with .ts".to_string())
})?)?);
@@ -588,11 +608,10 @@ struct DeploymentStatus {
lock_error_logs: Option<String>,
}
async fn get_deployment_status(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, hash)): Path<(String, ScriptHash)>,
) -> JsonResult<DeploymentStatus> {
let mut tx = user_db.begin(&authed).await?;
let mut tx = db.begin().await?;
let status_o: Option<DeploymentStatus> = sqlx::query_as!(
DeploymentStatus,
"SELECT lock, lock_error_logs FROM script WHERE hash = $1 AND workspace_id = $2",
@@ -761,25 +780,31 @@ async fn delete_script_by_path(
Ok(Json(script))
}
async fn parse_python_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_py::parse_python_signature(&code).map(Json)
#[derive(Debug, Serialize)]
#[serde(tag = "type")]
enum SigParsing {
Valid(MainArgSignature),
Invalid { error: String },
}
async fn parse_deno_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_ts::parse_deno_signature(&code).map(Json)
}
async fn parse_go_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_go::parse_go_sig(&code).map(Json)
fn result_to_sig_parsing(result: Result<MainArgSignature>) -> Json<SigParsing> {
match result {
Ok(sig) => Json(SigParsing::Valid(sig)),
Err(e) => Json(SigParsing::Invalid { error: e.to_string() }),
}
}
async fn parse_bash_code_to_jsonschema(
Json(code): Json<String>,
) -> JsonResult<windmill_parser::MainArgSignature> {
windmill_parser_bash::parse_bash_sig(&code).map(Json)
async fn parse_python_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_py::parse_python_signature(&code))
}
async fn parse_deno_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_ts::parse_deno_signature(&code, false))
}
async fn parse_go_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_go::parse_go_sig(&code))
}
async fn parse_bash_code_to_jsonschema(Json(code): Json<String>) -> Json<SigParsing> {
result_to_sig_parsing(windmill_parser_bash::parse_bash_sig(&code))
}

View File

@@ -76,6 +76,7 @@ pub fn global_service() -> Router {
.route("/tokens/create", post(create_token))
.route("/tokens/delete/:token_prefix", delete(delete_token))
.route("/tokens/list", get(list_tokens))
.route("/tokens/impersonate", post(impersonate))
.route("/usage", get(get_usage))
// .route("/list_invite_codes", get(list_invite_codes))
// .route("/create_invite_code", post(create_invite_code))
@@ -102,6 +103,10 @@ impl AuthCache {
AuthCache { cache: Cache::new(), db, superadmin_secret }
}
pub async fn invalidate(&self, w_id: &str, token: String) {
self.cache.remove(&(w_id.to_string(), token)).await;
}
pub async fn get_authed(&self, w_id: Option<String>, token: &str) -> Option<Authed> {
let key = (
w_id.as_ref().unwrap_or(&"".to_string()).to_string(),
@@ -356,6 +361,31 @@ pub struct Authed {
pub folders: Vec<(String, bool)>,
}
pub async fn maybe_refresh_folders(path: &str, w_id: &str, authed: Authed, db: &DB) -> Authed {
if authed.is_admin {
return authed;
}
let splitted = path.split('/').collect::<Vec<_>>();
if splitted.len() >= 2
&& splitted[0] == "f"
&& !authed.folders.iter().any(|(f, _)| f == splitted[1])
{
let name = &authed.username;
let groups = get_groups_for_user(w_id, name, db)
.await
.ok()
.unwrap_or_default();
let folders = get_folders_for_user(w_id, name, &groups, db)
.await
.ok()
.unwrap_or_default();
Authed { folders, ..authed }
} else {
authed
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Authed
where
@@ -564,6 +594,7 @@ pub struct TruncatedToken {
pub struct NewToken {
pub label: Option<String>,
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
pub impersonate_email: Option<String>,
}
#[derive(Deserialize)]
@@ -1009,10 +1040,6 @@ async fn accept_invite(
Extension(db): Extension<DB>,
Json(nu): Json<AcceptInvite>,
) -> Result<(StatusCode, String)> {
if &nu.username == "bot" {
return Err(Error::BadRequest("bot is a reserved username".to_string()));
}
if !VALID_USERNAME.is_match(&nu.username) {
return Err(windmill_common::error::Error::BadRequest(format!(
"Usermame can only contain alphanumeric characters and underscores"
@@ -1296,11 +1323,12 @@ async fn create_user(
Authed { email, .. }: Authed,
Extension(db): Extension<DB>,
Extension(argon2): Extension<Arc<Argon2<'_>>>,
Json(nu): Json<NewUser>,
Json(mut nu): Json<NewUser>,
) -> Result<(StatusCode, String)> {
let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?;
nu.email = nu.email.to_lowercase();
if nu.email == SUPERADMIN_SECRET_EMAIL {
return Err(Error::BadRequest(
@@ -1592,7 +1620,7 @@ async fn login(
Json(Login { email, password }): Json<Login>,
) -> Result<String> {
let mut tx = db.begin().await?;
let email = email.to_lowercase();
let email_w_h: Option<(String, String, bool, bool)> = sqlx::query_as(
"SELECT email, password_hash, super_admin, first_time_user FROM password WHERE email = $1 AND login_type = \
'password'",
@@ -1714,6 +1742,58 @@ async fn create_token(
Ok((StatusCode::CREATED, token))
}
async fn impersonate(
Extension(db): Extension<DB>,
Authed { email, username, .. }: Authed,
Json(new_token): Json<NewToken>,
) -> Result<(StatusCode, String)> {
let token = rd_string(30);
let mut tx = db.begin().await?;
require_super_admin(&mut tx, &email).await?;
if new_token.impersonate_email.is_none() {
return Err(Error::BadRequest(
"impersonate_username is required".to_string(),
));
}
let impersonated = new_token.impersonate_email.unwrap();
let is_super_admin = sqlx::query_scalar!(
"SELECT super_admin FROM password WHERE email = $1",
impersonated
)
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
sqlx::query!(
"INSERT INTO token
(token, email, label, expiration, super_admin)
VALUES ($1, $2, $3, $4, $5)",
token,
impersonated,
new_token.label,
new_token.expiration,
is_super_admin
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&username,
"users.impersonate",
ActionKind::Delete,
&"global",
Some(&token[0..10]),
Some([("impersonated", &format!("{impersonated}")[..])].into()),
)
.instrument(tracing::info_span!("token", email = &impersonated))
.await?;
tx.commit().await?;
Ok((StatusCode::CREATED, token))
}
async fn list_tokens(
Extension(db): Extension<DB>,
Authed { email, .. }: Authed,
@@ -1814,6 +1894,17 @@ pub async fn delete_expired_items_perdiodically(
Err(e) => tracing::error!("Error deleting token: {}", e.to_string()),
}
let pip_resolution_r = sqlx::query_scalar!(
"DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash",
)
.fetch_all(db)
.await;
match pip_resolution_r {
Ok(res) => tracing::debug!("deleted {} pip_resolution: {:?}", res.len(), res),
Err(e) => tracing::error!("Error deleting pip_resolution: {}", e.to_string()),
}
let magic_links_deleted_r: std::result::Result<Vec<String>, _> = sqlx::query_scalar(
"DELETE FROM magic_link WHERE expiration <= now()
RETURNING concat(substring(token for 10), '*****')",

View File

@@ -9,7 +9,7 @@
use crate::{
db::{UserDB, DB},
oauth2::_refresh_token,
users::{require_owner_of_path, Authed},
users::{maybe_refresh_folders, require_owner_of_path, Authed},
webhook_util::{WebhookMessage, WebhookShared},
};
/*
@@ -206,12 +206,15 @@ async fn check_path_conflict<'c>(
async fn create_variable(
authed: Authed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Query(AlreadyEncrypted { already_encrypted }): Query<AlreadyEncrypted>,
Json(variable): Json<CreateVariable>,
) -> Result<(StatusCode, String)> {
let authed = maybe_refresh_folders(&variable.path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;
check_path_conflict(&mut tx, &w_id, &variable.path).await?;
@@ -330,6 +333,7 @@ async fn update_variable(
use sql_builder::prelude::*;
let path = path.to_path();
let authed = maybe_refresh_folders(&path, &w_id, authed, &db).await;
let mut tx = user_db.begin(&authed).await?;

View File

@@ -2,6 +2,7 @@ use std::time::Duration;
use serde::Serialize;
use tokio::{select, sync::mpsc, time::interval};
use windmill_common::METRICS_ENABLED;
use crate::db::DB;
@@ -89,9 +90,9 @@ impl WebhookShared {
};
let webook_opt = url_guard.value();
if let Some(url) = webook_opt {
let timer = WEBHOOK_REQUEST_COUNT.start_timer();
let timer = if *METRICS_ENABLED { Some(WEBHOOK_REQUEST_COUNT.start_timer()) } else { None };
let _ = client.post(url).json(&message).send().await;
timer.stop_and_record();
timer.map(|x| x.stop_and_record());
drop(url_guard);
}
},

View File

@@ -944,10 +944,12 @@ async fn invite_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceInvite>,
Json(mut nu): Json<NewWorkspaceInvite>,
) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?;
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?;
sqlx::query!(
@@ -983,9 +985,10 @@ async fn add_user(
Authed { username, is_admin, .. }: Authed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
Json(nu): Json<NewWorkspaceUser>,
Json(mut nu): Json<NewWorkspaceUser>,
) -> Result<(StatusCode, String)> {
require_admin(is_admin, &username)?;
nu.email = nu.email.to_lowercase();
let mut tx = db.begin().await?;
if !VALID_USERNAME.is_match(&nu.username) {

View File

@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::net::SocketAddr;
use std::{net::SocketAddr, sync::Arc};
use error::Error;
@@ -30,7 +30,19 @@ pub const DEFAULT_MAX_CONNECTIONS_SERVER: u32 = 50;
pub const DEFAULT_MAX_CONNECTIONS_WORKER: u32 = 3;
lazy_static::lazy_static! {
pub static ref METRICS_ADDR: Option<SocketAddr> = std::env::var("METRICS_ADDR")
.ok()
.map(|s| {
s.parse::<bool>()
.map(|b| b.then(|| SocketAddr::from(([0, 0, 0, 0], 8001))))
.or_else(|_| s.parse::<SocketAddr>().map(Some))
})
.transpose().ok()
.flatten()
.flatten();
pub static ref METRICS_ENABLED: bool = METRICS_ADDR.is_some();
pub static ref BASE_URL: String = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost".to_string());
pub static ref IS_READY: Arc<std::sync::atomic::AtomicBool> = Arc::new(std::sync::atomic::AtomicBool::new(false));
}
#[cfg(feature = "tokio")]
@@ -58,14 +70,31 @@ pub async fn shutdown_signal(tx: tokio::sync::broadcast::Sender<()>) -> anyhow::
pub async fn serve_metrics(
addr: SocketAddr,
mut rx: tokio::sync::broadcast::Receiver<()>,
ready_worker_endpoint: bool,
) -> Result<(), hyper::Error> {
use std::sync::atomic::Ordering;
use axum::{routing::get, Router};
axum::Server::bind(&addr)
.serve(
Router::new()
.route("/metrics", get(metrics))
.into_make_service(),
use hyper::StatusCode;
let router = Router::new().route("/metrics", get(metrics));
let router = if ready_worker_endpoint {
router.route(
"/ready",
get(|| async {
if IS_READY.load(Ordering::Relaxed) {
(StatusCode::OK, "ready")
} else {
(StatusCode::INTERNAL_SERVER_ERROR, "not ready")
}
}),
)
} else {
router
};
axum::Server::bind(&addr)
.serve(router.into_make_service())
.with_graceful_shutdown(async {
rx.recv().await.ok();
println!("Graceful shutdown of metrics");

View File

@@ -17,7 +17,7 @@ pub struct Schedule {
pub edited_by: String,
pub edited_at: DateTime<chrono::Utc>,
pub schedule: String,
pub offset_: i32,
pub timezone: String,
pub enabled: bool,
pub script_path: String,
pub is_flow: bool,

View File

@@ -8,6 +8,7 @@
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
@@ -118,3 +119,9 @@ pub fn rd_string(len: usize) -> String {
.map(char::from)
.collect()
}
pub fn calculate_hash(s: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(s);
format!("{:x}", hasher.finalize())
}

View File

@@ -25,6 +25,7 @@ serde_json.workspace = true
ulid.workspace = true
uuid.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
hex.workspace = true
reqwest.workspace = true
lazy_static.workspace = true

View File

@@ -22,6 +22,7 @@ use windmill_common::{
flows::{FlowModule, FlowModuleValue, FlowValue},
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
utils::StripPath,
METRICS_ENABLED,
};
lazy_static::lazy_static! {
@@ -146,7 +147,7 @@ pub async fn pull(
.fetch_optional(db)
.await?;
if job.is_some() {
if job.is_some() && *METRICS_ENABLED {
QUEUE_PULL_COUNT.inc();
}
@@ -211,7 +212,9 @@ pub async fn delete_job(
w_id: &str,
job_id: Uuid,
) -> windmill_common::error::Result<()> {
QUEUE_DELETE_COUNT.inc();
if *METRICS_ENABLED {
QUEUE_DELETE_COUNT.inc();
}
let job_removed = sqlx::query_scalar!(
"DELETE FROM queue WHERE workspace_id = $1 AND id = $2 RETURNING 1",
w_id,
@@ -542,7 +545,9 @@ pub async fn push<'c>(
.await
.map_err(|e| Error::InternalErr(format!("Could not insert into queue {job_id}: {e}")))?;
// TODO: technically the job isn't queued yet, as the transaction can be rolled back. Should be solved when moving these metrics to the queue abstraction.
QUEUE_PUSH_COUNT.inc();
if *METRICS_ENABLED {
QUEUE_PUSH_COUNT.inc();
}
{
let uuid_string = job_id.to_string();

View File

@@ -6,10 +6,9 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::str::FromStr;
use chrono::Duration;
use crate::{push, JobPayload};
use sqlx::{query_scalar, Postgres, Transaction};
use std::str::FromStr;
use windmill_common::{
error::{self, Result},
schedule::Schedule,
@@ -17,8 +16,6 @@ use windmill_common::{
utils::{now_from_db, StripPath},
};
use crate::{push, JobPayload};
pub async fn push_scheduled_job<'c>(
mut tx: Transaction<'c, Postgres>,
schedule: Schedule,
@@ -26,13 +23,21 @@ pub async fn push_scheduled_job<'c>(
let sched = cron::Schedule::from_str(&schedule.schedule)
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
let offset = Duration::minutes(schedule.offset_.into());
let now = now_from_db(&mut tx).await?;
let tz = chrono_tz::Tz::from_str(&schedule.timezone)
.map_err(|e| error::Error::BadRequest(e.to_string()))?;
let now = now_from_db(&mut tx).await?.with_timezone(&tz);
let next = sched
.after(&(now - offset + Duration::seconds(1)))
.after(&now)
.next()
.expect("a schedule should have a next event")
+ offset;
.expect("a schedule should have a next event");
// println!("next event ({:?}): {}", tz, next);
// println!("next event(UTC): {}", next.with_timezone(&chrono::Utc));
// Scheduled events must be stored in the database in UTC
let next = next.with_timezone(&chrono::Utc);
let already_exists: bool = query_scalar!(
"SELECT EXISTS (SELECT 1 FROM queue WHERE workspace_id = $1 AND schedule_path = $2 AND scheduled_for = $3)",

View File

@@ -1,129 +0,0 @@
name: "deno run script"
mode: ONCE
hostname: "deno"
log_level: ERROR
rlimit_as: 16000
rlimit_cpu: 1000
rlimit_fsize: 1000
rlimit_nofile: 10000
cwd: "/tmp"
clone_newnet: false
clone_newuser: {CLONE_NEWUSER}
keep_caps: false
keep_env: true
mount_proc: true
mount {
src: "/bin"
dst: "/bin"
is_bind: true
}
mount {
src: "/lib"
dst: "/lib"
is_bind: true
}
mount {
src: "/lib64"
dst: "/lib64"
is_bind: true
}
mount {
src: "/usr"
dst: "/usr"
is_bind: true
}
mount {
src: "/dev/null"
dst: "/dev/null"
is_bind: true
rw: true
}
mount {
dst: "/tmp"
fstype: "tmpfs"
rw: true
options: "size=500000000"
}
mount {
src: "{JOB_DIR}/wrapper.ts"
dst: "/tmp/wrapper.ts"
is_bind: true
}
mount {
src: "{JOB_DIR}/main.ts"
dst: "/tmp/main.ts"
is_bind: true
}
mount {
src: "{JOB_DIR}/import_map.json"
dst: "/tmp/import_map.json"
is_bind: true
}
mount {
src: "{JOB_DIR}/args.json"
dst: "/tmp/args.json"
is_bind: true
}
mount {
src: "{JOB_DIR}/result.json"
dst: "/tmp/result.json"
is_bind: true
rw: true
}
mount {
src: "/etc"
dst: "/etc"
is_bind: true
}
mount {
src: "/dev/random"
dst: "/dev/random"
is_bind: true
}
mount {
src: "/dev/urandom"
dst: "/dev/urandom"
is_bind: true
}
mount {
src: "{CACHE_DIR}"
dst: "/tmp/.cache/deno"
is_bind: true
rw: true
mandatory: false
}
{SHARED_MOUNT}
iface_no_lo: true
envar: "DENO_DIR=/tmp/.cache/deno"
envar: "NO_COLOR=true"
envar: "HOME=/tmp"

View File

@@ -6,10 +6,14 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::time::SystemTime;
use sqlx::{Pool, Postgres, Transaction};
use tracing::instrument;
use uuid::Uuid;
use windmill_common::{error::Error, flow_status::FlowStatusModule, schedule::Schedule};
use windmill_common::{
error::Error, flow_status::FlowStatusModule, schedule::Schedule, METRICS_ENABLED,
};
use windmill_queue::{delete_job, schedule::get_schedule_opt, JobKind, QueuedJob, CLOUD_HOSTED};
#[instrument(level = "trace", skip_all)]
@@ -20,7 +24,9 @@ pub async fn add_completed_job_error(
e: serde_json::Value,
metrics: Option<crate::worker::Metrics>,
) -> Result<serde_json::Value, Error> {
metrics.map(|m| m.worker_execution_failed.inc());
if *METRICS_ENABLED {
metrics.map(|m| m.worker_execution_failed.inc());
}
let result = serde_json::json!({ "error": e });
let _ = add_completed_job(db, &queued_job, false, false, result.clone(), logs).await?;
Ok(result)
@@ -153,6 +159,8 @@ pub async fn add_completed_job(
.execute(&mut tx)
.await
.map_err(|e| Error::InternalErr(format!("Could not add completed job {job_id}: {e}")))?;
println!("{:?}", SystemTime::now());
let _ = delete_job(db, &queued_job.workspace_id, job_id).await?;
if !queued_job.is_flow_step
&& queued_job.job_kind != JobKind::Flow
@@ -232,7 +240,7 @@ pub async fn schedule_again_if_scheduled<'c>(
edited_by: schedule.edited_by,
edited_at: schedule.edited_at,
schedule: schedule.schedule,
offset_: schedule.offset_,
timezone: schedule.timezone,
enabled: schedule.enabled,
script_path: schedule.script_path,
is_flow: schedule.is_flow,

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,6 @@ use windmill_queue::{
canceled_job_to_result, get_queued_job, push, JobPayload, QueuedJob, RawCode,
};
#[async_recursion]
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion(
db: &DB,
@@ -51,57 +50,125 @@ pub async fn update_flow_status_after_job_completion(
stop_early_override: Option<bool>,
base_internal_url: &str,
) -> error::Result<()> {
tracing::debug!("UPDATE FLOW STATUS: {flow:?} {success} {result:?} {w_id}");
let mut tx = db.begin().await?;
let old_status_json = sqlx::query_scalar!(
"SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2",
// this is manual tailrecursion because async_recursion blows up the stack
let mut depth = 0;
let mut rec = update_flow_status_after_job_completion_internal(
db,
client,
flow,
w_id
job_id_for_status,
w_id,
success,
result,
metrics.clone(),
unrecoverable,
same_worker_tx.clone(),
worker_dir,
stop_early_override,
base_internal_url,
depth,
)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!(
"fetching flow status {flow} while reporting {success} {result:?}: {e}"
))
})?
.ok_or_else(|| Error::InternalErr(format!("requiring a previous status")))?;
.await?;
while let Some(nrec) = rec {
depth += 1;
rec = update_flow_status_after_job_completion_internal(
db,
client,
nrec.flow,
&nrec.job_id_for_status,
w_id,
nrec.success,
nrec.result,
metrics.clone(),
false,
same_worker_tx.clone(),
worker_dir,
nrec.stop_early_override,
base_internal_url,
depth,
)
.await?;
}
Ok(())
}
pub struct RecUpdateFlowStatusAfterJobCompletion {
flow: uuid::Uuid,
job_id_for_status: Uuid,
success: bool,
result: serde_json::Value,
stop_early_override: Option<bool>,
}
// #[instrument(level = "trace", skip_all)]
pub async fn update_flow_status_after_job_completion_internal(
db: &DB,
client: &AuthedClient,
flow: uuid::Uuid,
job_id_for_status: &Uuid,
w_id: &str,
success: bool,
result: serde_json::Value,
metrics: Option<worker::Metrics>,
unrecoverable: bool,
same_worker_tx: Sender<Uuid>,
worker_dir: &str,
stop_early_override: Option<bool>,
base_internal_url: &str,
depth: u8,
) -> error::Result<Option<RecUpdateFlowStatusAfterJobCompletion>> {
let (should_continue_flow, flow_job, stop_early, skip_if_stop_early, nresult) = {
tracing::debug!("UPDATE FLOW STATUS: {flow:?} {success} {result:?} {w_id} {depth}");
let old_status = serde_json::from_value::<FlowStatus>(old_status_json)
.ok()
.ok_or_else(|| {
Error::InternalErr(format!("requiring status to be parsabled as FlowStatus"))
})?;
let mut tx = db.begin().await?;
let module_index = usize::try_from(old_status.step).ok();
let old_status_json = sqlx::query_scalar!(
"SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2",
flow,
w_id
)
.fetch_one(&mut tx)
.await
.map_err(|e| {
Error::InternalErr(format!(
"fetching flow status {flow} while reporting {success} {result:?}: {e}"
))
})?
.ok_or_else(|| Error::InternalErr(format!("requiring a previous status")))?;
let module_status = module_index
.and_then(|i| old_status.modules.get(i))
.unwrap_or(&old_status.failure_module.module_status);
let old_status = serde_json::from_value::<FlowStatus>(old_status_json)
.ok()
.ok_or_else(|| {
Error::InternalErr(format!("requiring status to be parsabled as FlowStatus"))
})?;
tracing::debug!("UPDATE FLOW STATUS 2: {module_index:#?} {module_status:#?} {old_status:#?} ");
let module_index = usize::try_from(old_status.step).ok();
let skip_loop_failures = if matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
) {
compute_skip_loop_failures(flow, old_status.step, &mut tx)
.await?
.unwrap_or(false)
} else {
false
};
let module_status = module_index
.and_then(|i| old_status.modules.get(i))
.unwrap_or(&old_status.failure_module.module_status);
let is_failure_step = old_status.step >= old_status.modules.len() as i32;
tracing::debug!(
"UPDATE FLOW STATUS 2: {module_index:#?} {module_status:#?} {old_status:#?} "
);
let (mut stop_early, skip_if_stop_early) = if let Some(se) = stop_early_override {
(true, se)
} else if is_failure_step {
(false, false)
} else {
let r = sqlx::query!(
let skip_loop_failures = if matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
) {
compute_skip_loop_failures(flow, old_status.step, &mut tx)
.await?
.unwrap_or(false)
} else {
false
};
let is_failure_step = old_status.step >= old_status.modules.len() as i32;
let (mut stop_early, skip_if_stop_early) = if let Some(se) = stop_early_override {
(true, se)
} else if is_failure_step {
(false, false)
} else {
let r = sqlx::query!(
"
SELECT raw_flow->'modules'->$1::int->'stop_after_if'->>'expr' as stop_early_expr,
(raw_flow->'modules'->$1::int->'stop_after_if'->>'skip_if_stopped')::bool as skip_if_stopped,
@@ -116,37 +183,39 @@ pub async fn update_flow_status_after_job_completion(
.await
.map_err(|e| Error::InternalErr(format!("retrieval of stop_early_expr from state: {e}")))?;
let stop_early = success
&& if let Some(expr) = r.stop_early_expr.clone() {
compute_bool_from_expr(expr, &r.args, result.clone(), None, Some(client)).await?
} else {
false
};
(stop_early, r.skip_if_stopped.unwrap_or(false))
};
let stop_early = success
&& if let Some(expr) = r.stop_early_expr.clone() {
compute_bool_from_expr(expr, &r.args, result.clone(), None, Some(client))
.await?
} else {
false
};
(stop_early, r.skip_if_stopped.unwrap_or(false))
};
let skip_branch_failure = match module_status {
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, .. }), ..
} => compute_skip_branchall_failure(flow, old_status.step, *branch, &mut tx)
.await?
.unwrap_or(false),
_ => false,
};
let skip_branch_failure = match module_status {
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, .. }),
..
} => compute_skip_branchall_failure(flow, old_status.step, *branch, &mut tx)
.await?
.unwrap_or(false),
_ => false,
};
let skip_failure = skip_branch_failure || skip_loop_failures;
let skip_failure = skip_branch_failure || skip_loop_failures;
let (inc_step_counter, new_status) = match module_status {
FlowStatusModule::InProgress {
iterator,
branchall,
parallel,
flow_jobs: Some(jobs),
..
} if *parallel => {
let (nindex, len) = match (iterator, branchall) {
(Some(Iterator { itered, .. }), _) => {
let nindex = sqlx::query_scalar!(
let (inc_step_counter, new_status) = match module_status {
FlowStatusModule::InProgress {
iterator,
branchall,
parallel,
flow_jobs: Some(jobs),
..
} if *parallel => {
let (nindex, len) = match (iterator, branchall) {
(Some(Iterator { itered, .. }), _) => {
let nindex = sqlx::query_scalar!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'iterator', 'index'], ((flow_status->'modules'->$1::int->'iterator'->>'index')::int + 1)::text::jsonb)
@@ -159,10 +228,10 @@ pub async fn update_flow_status_after_job_completion(
.fetch_one(&mut tx)
.await?
.ok_or_else(|| Error::InternalErr(format!("requiring an index in InProgress")))?;
(nindex, itered.len() as i32)
}
(_, Some(BranchAllStatus { len, .. })) => {
let nindex = sqlx::query_scalar!(
(nindex, itered.len() as i32)
}
(_, Some(BranchAllStatus { len, .. })) => {
let nindex = sqlx::query_scalar!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'branchall', 'branch'], ((flow_status->'modules'->$1::int->'branchall'->>'branch')::int + 1)::text::jsonb)
@@ -175,269 +244,292 @@ pub async fn update_flow_status_after_job_completion(
.fetch_one(&mut tx)
.await?
.ok_or_else(|| Error::InternalErr(format!("requiring an index in InProgress")))?;
(nindex, *len as i32)
}
_ => Err(Error::InternalErr(format!(
"unexpected status for parallel module"
)))?,
};
if nindex == len {
let new_status = if skip_loop_failures
|| sqlx::query_scalar!(
"
(nindex, *len as i32)
}
_ => Err(Error::InternalErr(format!(
"unexpected status for parallel module"
)))?,
};
if nindex == len {
let new_status = if skip_loop_failures
|| sqlx::query_scalar!(
"
SELECT success
FROM completed_job
WHERE id = ANY($1)
",
jobs.as_slice(),
)
.fetch_all(&mut tx)
.await?
.into_iter()
.all(|x| x)
{
FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
branch_chosen: None,
approvers: vec![],
}
jobs.as_slice(),
)
.fetch_all(&mut tx)
.await?
.into_iter()
.all(|x| x)
{
FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
branch_chosen: None,
approvers: vec![],
}
} else {
FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
branch_chosen: None,
}
};
(true, Some(new_status))
} else {
FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs: Some(jobs.clone()),
branch_chosen: None,
}
};
(true, Some(new_status))
} else {
tx.commit().await?;
return Ok(());
}
}
FlowStatusModule::InProgress {
iterator: Some(windmill_common::flow_status::Iterator { index, itered, .. }),
..
} if (*index + 1 < itered.len() && (success || skip_loop_failures)) && !stop_early => {
(false, None)
}
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, len, .. }),
..
} if branch.to_owned() < len - 1 && (success || skip_branch_failure) => (false, None),
_ => {
if stop_early
&& matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
)
{
// if we're stopping early inside a loop, we just want to break the loop instead
stop_early = false;
}
let (flow_jobs, branch_chosen) = match module_status {
FlowStatusModule::InProgress { flow_jobs, branch_chosen, .. } => {
(flow_jobs.clone(), branch_chosen.clone())
tx.commit().await?;
return Ok(None);
}
_ => (None, None),
};
if success || (flow_jobs.is_some() && (skip_loop_failures || skip_branch_failure)) {
(
true,
Some(FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
branch_chosen,
approvers: vec![],
}),
)
} else {
(
false,
Some(FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
branch_chosen,
}),
)
}
}
};
FlowStatusModule::InProgress {
iterator: Some(windmill_common::flow_status::Iterator { index, itered, .. }),
..
} if (*index + 1 < itered.len() && (success || skip_loop_failures)) && !stop_early => {
(false, None)
}
FlowStatusModule::InProgress {
branchall: Some(BranchAllStatus { branch, len, .. }),
..
} if branch.to_owned() < len - 1 && (success || skip_branch_failure) => (false, None),
_ => {
if stop_early
&& matches!(
module_status,
FlowStatusModule::InProgress { iterator: Some(_), .. }
)
{
// if we're stopping early inside a loop, we just want to break the loop instead
stop_early = false;
}
let (flow_jobs, branch_chosen) = match module_status {
FlowStatusModule::InProgress { flow_jobs, branch_chosen, .. } => {
(flow_jobs.clone(), branch_chosen.clone())
}
_ => (None, None),
};
if success || (flow_jobs.is_some() && (skip_loop_failures || skip_branch_failure)) {
(
true,
Some(FlowStatusModule::Success {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
branch_chosen,
approvers: vec![],
}),
)
} else {
(
false,
Some(FlowStatusModule::Failure {
id: module_status.id(),
job: job_id_for_status.clone(),
flow_jobs,
branch_chosen,
}),
)
}
}
};
let step_counter = if inc_step_counter {
sqlx::query!(
"
let step_counter = if inc_step_counter {
sqlx::query!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['step'], $1)
WHERE id = $2
",
json!(old_status.step + 1),
flow
)
.execute(&mut tx)
.await?;
old_status.step + 1
} else {
old_status.step
};
json!(old_status.step + 1),
flow
)
.execute(&mut tx)
.await?;
old_status.step + 1
} else {
old_status.step
};
/* is_last_step is true when the step_counter (the next step index) is an invalid index */
let is_last_step = usize::try_from(step_counter)
.map(|i| !(..old_status.modules.len()).contains(&i))
.unwrap_or(true);
/* is_last_step is true when the step_counter (the next step index) is an invalid index */
let is_last_step = usize::try_from(step_counter)
.map(|i| !(..old_status.modules.len()).contains(&i))
.unwrap_or(true);
if let Some(new_status) = new_status.as_ref() {
if is_failure_step {
let parent_module = sqlx::query_scalar!(
if let Some(new_status) = new_status.as_ref() {
if is_failure_step {
let parent_module = sqlx::query_scalar!(
"SELECT flow_status->'failure_module'->>'parent_module' FROM queue WHERE id = $1",
flow
)
.fetch_one(&mut tx)
.await?;
.fetch_one(&mut tx)
.await?;
sqlx::query!(
"
sqlx::query!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['failure_module'], $1)
WHERE id = $2
",
json!(FlowStatusModuleWParent { parent_module, module_status: new_status.clone() }),
flow
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2)
WHERE id = $3
",
old_status.step.to_string(),
json!(new_status),
flow
)
.execute(&mut tx)
.await?;
if let Some(job_result) = new_status.job_result() {
sqlx::query!(
"
UPDATE queue
SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)
WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id
",
new_status.id(),
json!(job_result),
json!(FlowStatusModuleWParent {
parent_module,
module_status: new_status.clone()
}),
flow
)
.execute(&mut tx)
.await?;
} else {
sqlx::query!(
"
UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT], $2)
WHERE id = $3
",
old_status.step.to_string(),
json!(new_status),
flow
)
.execute(&mut tx)
.await?;
if let Some(job_result) = new_status.job_result() {
sqlx::query!(
"
UPDATE queue
SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)
WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id
",
new_status.id(),
json!(job_result),
flow
)
.execute(&mut tx)
.await?;
}
}
}
}
let result = match &new_status {
Some(FlowStatusModule::Success { flow_jobs: Some(jobs), .. })
| Some(FlowStatusModule::Failure { flow_jobs: Some(jobs), .. }) => {
let results = sqlx::query!(
"
let nresult = match &new_status {
Some(FlowStatusModule::Success { flow_jobs: Some(jobs), .. })
| Some(FlowStatusModule::Failure { flow_jobs: Some(jobs), .. }) => {
let results = sqlx::query!(
"
SELECT result, id
FROM completed_job
WHERE id = ANY($1)
AND workspace_id = $2
",
jobs.as_slice(),
w_id
)
.fetch_all(&mut tx)
.await?
.into_iter()
.map(|r| (r.id, r.result))
.collect::<HashMap<_, _>>();
jobs.as_slice(),
w_id
)
.fetch_all(&mut tx)
.await?
.into_iter()
.map(|r| (r.id, r.result))
.collect::<HashMap<_, _>>();
let results = jobs
.iter()
.map(|j| {
results
.get(j)
.ok_or_else(|| Error::InternalErr(format!("missing job result for {}", j)))
})
.collect::<Result<Vec<_>, _>>()?;
let results = jobs
.iter()
.map(|j| {
results.get(j).ok_or_else(|| {
Error::InternalErr(format!("missing job result for {}", j))
})
})
.collect::<Result<Vec<_>, _>>()?;
json!(results)
}
_ => result,
};
json!(results)
}
_ => result,
};
if matches!(&new_status, Some(FlowStatusModule::Success { .. })) {
sqlx::query(
"
if matches!(&new_status, Some(FlowStatusModule::Success { .. })) {
sqlx::query(
"
UPDATE queue
SET flow_status = flow_status - 'retry'
WHERE id = $1
RETURNING flow_status
",
)
.bind(flow)
.execute(&mut tx)
.await
.context("remove flow status retry")?;
}
let flow_job = get_queued_job(flow, w_id, &mut tx)
.await?
.ok_or_else(|| Error::InternalErr(format!("requiring flow to be in the queue")))?;
let job_root = flow_job
.root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
let raw_flow = flow_job.parse_raw_flow();
let module = raw_flow.as_ref().and_then(|module| {
module_index.and_then(|i| module.modules.get(i).or(module.failure_module.as_ref()))
});
let should_continue_flow = match success {
_ if stop_early => false,
_ if flow_job.canceled => false,
true => !is_last_step,
false if unrecoverable => false,
false if skip_failure => !is_last_step,
false
if next_retry(
&module.and_then(|m| m.retry.clone()).unwrap_or_default(),
&old_status.retry,
)
.is_some() =>
{
true
.bind(flow)
.execute(&mut tx)
.await
.context("remove flow status retry")?;
}
false if has_failure_module(flow, &mut tx).await? && !is_failure_step => true,
false => false,
};
if old_status.step == 0
&& !flow_job.is_flow_step
&& flow_job.schedule_path.is_some()
&& flow_job.script_path.is_some()
{
tx = schedule_again_if_scheduled(
tx,
db,
flow_job.schedule_path.as_ref().unwrap(),
flow_job.script_path.as_ref().unwrap(),
&w_id,
let flow_job = get_queued_job(flow, w_id, &mut tx)
.await?
.ok_or_else(|| Error::InternalErr(format!("requiring flow to be in the queue")))?;
let job_root = flow_job
.root_job
.map(|x| x.to_string())
.unwrap_or_else(|| "none".to_string());
tracing::info!(id = %flow_job.id, root_id = %job_root, "update flow status");
let module = {
let raw_flow = flow_job.parse_raw_flow();
if let Some(raw_flow) = raw_flow {
if let Some(i) = module_index {
if let Some(module) = raw_flow.modules.get(i) {
Some(module.clone())
} else {
raw_flow.failure_module
}
} else {
None
}
} else {
None
}
};
let should_continue_flow = match success {
_ if stop_early => false,
_ if flow_job.canceled => false,
true => !is_last_step,
false if unrecoverable => false,
false if skip_failure => !is_last_step,
false
if next_retry(
&module.and_then(|m| m.retry.clone()).unwrap_or_default(),
&old_status.retry,
)
.is_some() =>
{
true
}
false if has_failure_module(flow, &mut tx).await? && !is_failure_step => true,
false => false,
};
if old_status.step == 0
&& !flow_job.is_flow_step
&& flow_job.schedule_path.is_some()
&& flow_job.script_path.is_some()
{
tx = schedule_again_if_scheduled(
tx,
db,
flow_job.schedule_path.as_ref().unwrap(),
flow_job.script_path.as_ref().unwrap(),
&w_id,
)
.await?;
}
tx.commit().await?;
(
should_continue_flow,
flow_job,
stop_early,
skip_if_stop_early,
nresult,
)
.await?;
}
tx.commit().await?;
};
let done = if !should_continue_flow {
let logs = if flow_job.canceled {
@@ -464,7 +556,7 @@ pub async fn update_flow_status_after_job_completion(
&flow_job,
success,
stop_early && skip_if_stop_early,
result.clone(),
nresult.clone(),
logs,
)
.await?;
@@ -475,7 +567,7 @@ pub async fn update_flow_status_after_job_completion(
&flow_job,
db,
client,
result.clone(),
nresult.clone(),
same_worker_tx.clone(),
worker_dir,
base_internal_url,
@@ -503,30 +595,22 @@ pub async fn update_flow_status_after_job_completion(
}
if let Some(parent_job) = flow_job.parent_job {
return Ok(update_flow_status_after_job_completion(
db,
client,
parent_job,
&flow,
w_id,
return Ok(Some(RecUpdateFlowStatusAfterJobCompletion {
flow: parent_job,
job_id_for_status: flow,
success,
result,
metrics,
false,
same_worker_tx.clone(),
worker_dir,
if stop_early {
result: nresult,
stop_early_override: if stop_early {
Some(skip_if_stop_early)
} else {
None
},
base_internal_url,
)
.await?);
}));
}
Ok(None)
} else {
Ok(None)
}
Ok(())
}
async fn compute_skip_loop_failures<'c>(
@@ -1261,7 +1345,11 @@ async fn push_next_flow_job(
Ok(v) => (Some(v), None),
Err(e) => (None, Some(e)),
};
let root_job = flow_job.root_job.or_else(|| Some(flow_job.id));
let root_job = if matches!(module.value, FlowModuleValue::Flow { .. }) {
None
} else {
flow_job.root_job.or_else(|| Some(flow_job.id))
};
let (uuid, inner_tx) = push(
tx,
&flow_job.workspace_id,

View File

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

View File

@@ -335,21 +335,15 @@ async function pull(
}
}
} catch {}
if (change.path.endsWith(".json")) {
const diffs = microdiff(
JSON.parse(change.before),
JSON.parse(change.after),
{ cyclesFix: false }
);
console.log(
`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`
);
await applyDiff(diffs, target);
if (!change.path.endsWith(".json")) {
console.log(`Editing script content of ${change.path}`);
} else {
console.log(`Editing script ${change.path}`);
await Deno.writeTextFile(target, change.after);
console.log(
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`
);
}
await Deno.writeTextFile(target, change.after);
if (!opts.raw) {
await ensureDir(path.dirname(stateTarget));
await Deno.copyFile(target, stateTarget);
@@ -425,31 +419,6 @@ async function pull(
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m");
console.log();
}
async function applyDiff(diffs: Difference[], file: string) {
ensureDir(path.dirname(file));
let json;
try {
json = JSON.parse(await Deno.readTextFile(file));
} catch {
json = {};
}
// TODO: Delegate the below to the object itself
// This would work by infering the type of `JSON` (which includes then statically typing it using decoverto) and then
// delegating the applying of the diffs to the object via an interface
for (const diff of diffs) {
if (diff.type === "CREATE") {
setValueByPath(json, diff.path, diff.value);
} else if (diff.type === "REMOVE") {
setValueByPath(json, diff.path, undefined);
} else if (diff.type === "CHANGE") {
setValueByPath(json, diff.path, diff.value);
}
}
await Deno.writeTextFile(file, JSON.stringify(json, undefined, " "), {
create: true,
});
}
}
function prettyChanges(changes: Change[]) {

View File

@@ -80,6 +80,31 @@ export async function getResource(
}
}
/**
* Get the full resource value by path
* @param path path of the resource, default to internal state path
* @param undefinedIfEmpty if the resource does not exist, return undefined instead of throwing an error
* @returns full resource
*/
export async function getFullResource(
path?: string,
undefinedIfEmpty?: boolean
): Promise<any> {
const workspace = getWorkspace();
path = path ?? getStatePath();
try {
const resource = await ResourceService.getResource({ workspace, path });
const value = await _transformLeaf(resource.value);
return { ...resource, value };
} catch (e: any) {
if (undefinedIfEmpty && e.status === 404) {
return undefined;
} else {
throw Error(`Resource not found at ${path} or not visible to you`);
}
}
}
export function getStatePath(): string {
const state_path = Deno.env.get("WM_STATE_PATH");
if (state_path === undefined) {

View File

@@ -1,21 +1,18 @@
version: '3.7'
version: "3.7"
services:
db:
image: postgres:14
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
ports:
- 5432:5432
# NOTE: if port 5432 are already in use locally, we'll also bind another port
- 5433:5432
expose:
- 5432
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: windmill
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
@@ -25,11 +22,11 @@ services:
deploy:
replicas: 1
restart: unless-stopped
ports:
- 8000:8000
expose:
- 8000
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
- BASE_URL=http://${WM_BASE_URL}
- BASE_URL=${WM_BASE_URL}
- RUST_LOG=info
## You can set the number of workers to > 0 and not need any separate worker service
- NUM_WORKERS=0
@@ -39,8 +36,8 @@ services:
db:
condition: service_healthy
# volumes:
# - ./oauth.json/:/usr/src/app/oauth.json
# - ./oauth.json/:/usr/src/app/oauth.json
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
deploy:
@@ -48,7 +45,7 @@ services:
restart: unless-stopped
environment:
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
- BASE_URL=http://${WM_BASE_URL}
- BASE_URL=${WM_BASE_URL}
- BASE_INTERNAL_URL=http://windmill_server:8000
- RUST_LOG=info
- NUM_WORKERS=1
@@ -67,8 +64,8 @@ services:
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest
restart: unless-stopped
ports:
- 3001:3001
expose:
- 3001
caddy:
image: caddy:2.5.2-alpine
@@ -79,7 +76,7 @@ services:
- 80:80
- 443:443
environment:
- BASE_URL=${WM_BASE_URL}
- BASE_URL=${CADDY_REVERSE_PROXY}
volumes:
db_data: null

View File

@@ -2,9 +2,11 @@ http://localhost {
bind {$ADDRESS}
reverse_proxy /api/* http://localhost:8000
reverse_proxy /* http://localhost:3000
reverse_proxy /ws/* http://localhost:3001
}
https://localhost {
bind {$ADDRESS}
reverse_proxy /api/* http://localhost:8000
reverse_proxy /ws/* http://localhost:3001
}

1
frontend/copy_workers.sh Executable file
View File

@@ -0,0 +1 @@
cp -r node_modules/monaco-editor-workers/dist/workers static

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.84.0",
"version": "1.86.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -16,7 +16,8 @@
},
"devDependencies": {
"@playwright/test": "^1.31.1",
"@sveltejs/adapter-static": "^1.0.0",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.1",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
"@tailwindcss/forms": "^0.5.3",
@@ -25,16 +26,17 @@
"@types/d3-zoom": "^3.0.2",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.18",
"@types/vscode": "~1.74.0",
"@types/vscode": "~1.76.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
"cssnano": "^6.0.0",
"d3-dag": "^0.11.5",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"monaco-editor-workers": "0.36.0",
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
@@ -47,10 +49,11 @@
"style-to-object": "^0.4.1",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
"svelte-awesome": "^3.0.0",
"svelte-awesome": "^3.2.0",
"svelte-awesome-color-picker": "^2.4.1",
"svelte-check": "^3.0.2",
"svelte-highlight": "^6.2.1",
"svelte-highlight": "^7.2.1",
"svelte-multiselect": "^8.6.0",
"svelte-overlay": "^1.4.1",
"svelte-popperjs": "^1.3.2",
"svelte-preprocess": "^5.0.1",
@@ -67,12 +70,11 @@
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@leeoniya/ufuzzy": "^0.9.1",
"@leeoniya/ufuzzy": "^1.0.6",
"@popperjs/core": "^2.11.6",
"@redocly/json-to-json-schema": "^0.0.1",
"@tanstack/svelte-table": "^8.7.6",
"ag-grid-svelte": "^0.1.4",
"async-mutex": "^0.4.0",
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.0.0",
@@ -82,16 +84,16 @@
"highlight.js": "^11.7.0",
"lodash": "^4.17.21",
"lucide-svelte": "^0.115.0",
"monaco-editor": "0.33.0",
"monaco-editor-workers": "0.33.0",
"monaco-languageclient": "2.1.0",
"monaco-yaml": "^4.0.2",
"monaco-editor": "0.36.1",
"monaco-languageclient": "5.0.1",
"svelte-autosize": "^1.0.1",
"svelte-chartjs": "^3.1.0",
"svelte-portal": "^2.2.0",
"svelte-select": "^5.3.1",
"svelte-timezone-picker": "^2.0.3",
"tailwind-merge": "^1.9.1",
"vscode-ws-jsonrpc": "^2.0.1"
"vscode-languageclient": "^8.1.0",
"vscode-ws-jsonrpc": "3.0.0"
},
"peerDependencies": {
"svelte": "^3.55.1"

View File

@@ -370,7 +370,7 @@
<h2 class="mt-8 mb-4">Non OAuth APIs & Resources</h2>
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#if filteredConnectsManual}
{#each filteredConnectsManual as [key, instructions]}
{#each filteredConnectsManual as [key, _]}
<Button
size="sm"
variant="border"
@@ -414,7 +414,7 @@
</div>
{#if apiTokenApps[resource_type].img}
<div class="mt-4 w-full overflow-hidden">
<img class="m-auto max-h-60" alt="connect" src={apiTokenApps[resource_type].img} />
<img class="m-auto max-h-60" alt="connect" src={apiTokenApps[resource_type].img} />
</div>
{/if}
{/if}

View File

@@ -1,13 +1,11 @@
<script lang="ts">
import { truncate } from '$lib/utils'
import Tooltip from './Tooltip.svelte'
import json from 'svelte-highlight/languages/json'
import { Highlight } from 'svelte-highlight'
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Drawer from './common/drawer/Drawer.svelte'
import { truncate } from '$lib/utils'
import { DrawerContent } from './common'
import Drawer from './common/drawer/Drawer.svelte'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import Tooltip from './Tooltip.svelte'
export let value: any
let jsonViewer: Drawer

View File

@@ -3,32 +3,31 @@
faChevronDown,
faChevronUp,
faDollarSign,
faMinus,
faPlus
} from '@fortawesome/free-solid-svg-icons'
import { setInputCat as computeInputCat } from '$lib/utils'
import { Badge, Button } from './common'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import FieldHeader from './FieldHeader.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
import SchemaForm from './SchemaForm.svelte'
import type { SchemaProperty } from '$lib/common'
import SimpleEditor from './SimpleEditor.svelte'
import { setInputCat as computeInputCat } from '$lib/utils'
import { X } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import autosize from 'svelte-autosize'
import Toggle from './Toggle.svelte'
import Password from './Password.svelte'
import type VariableEditor from './VariableEditor.svelte'
import Icon from 'svelte-awesome'
import { fade } from 'svelte/transition'
import JsonEditor from './apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
import { Badge, Button } from './common'
import FieldHeader from './FieldHeader.svelte'
import type ItemPicker from './ItemPicker.svelte'
import NumberTypeNarrowing from './NumberTypeNarrowing.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import ObjectTypeNarrowing from './ObjectTypeNarrowing.svelte'
import Password from './Password.svelte'
import Range from './Range.svelte'
import JsonEditor from './apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
import { fade } from 'svelte/transition'
import { X } from 'lucide-svelte'
import ResourcePicker from './ResourcePicker.svelte'
import SchemaForm from './SchemaForm.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import StringTypeNarrowing from './StringTypeNarrowing.svelte'
import Toggle from './Toggle.svelte'
import type VariableEditor from './VariableEditor.svelte'
export let label: string = ''
export let value: any
@@ -42,7 +41,6 @@
export let required = false
export let pattern: undefined | string = undefined
export let valid = required ? false : true
export let maxRows = 10
export let enum_: string[] | undefined = undefined
export let disabled = false
export let editableSchema = false
@@ -63,8 +61,6 @@
let seeEditable: boolean = enum_ != undefined || pattern != undefined
const dispatch = createEventDispatcher()
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
$: validateInput(pattern, value)
let error: string = ''
@@ -168,6 +164,7 @@
let itemsLimit = 50
</script>
<!-- svelte-ignore a11y-autofocus -->
<div class="flex flex-col w-full min-w-[250px]">
<div>
{#if displayHeader}
@@ -175,6 +172,7 @@
{/if}
{#if editableSchema}
<div class="p-2 my-1 text-xs border-solid border border-gray-400">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class="underline"
on:click={() => {

View File

@@ -13,9 +13,11 @@
})
</script>
<div class="center-center min-h-screen p-4 relative bg-[#3b4252]">
<div class="border rounded-md shadow-md bg-white w-full max-w-[640px]
p-4 sm:py-8 sm:px-10 mb-6 md:mb-20 z-10">
<div class="center-center min-h-screen p-4 relative bg-gray-50">
<div
class="border rounded-md shadow-md bg-white w-full max-w-[640px]
p-4 sm:py-8 sm:px-10 mb-6 md:mb-20 z-10"
>
<div class="mb-10">
<h1 class="text-center">
{title}
@@ -30,13 +32,13 @@
</div>
{#if !disableLogo}
<div class="hidden lg:block absolute top-10 right-50">
<div class="animate-[spin_100s_linear_infinite] ">
<WindmillIcon white height="100px" width="100px" />
<div>
<WindmillIcon height="100px" width="100px" spin="slow" />
</div>
<h2 class="text-center pt-2 text-gray-100">Windmill</h2>
<h2 class="text-center pt-2 text-gray-800">Windmill</h2>
</div>
{/if}
<div class="absolute top-0 right-0 text-2xs text-gray-300 italic px-3 py-1">
<div class="absolute top-0 right-0 text-2xs text-gray-800 italic px-3 py-1">
<span class="font-mono">{version}</span>
</div>
</div>

View File

@@ -1,92 +1,328 @@
<script context="module">
export const OFFSET = new Date().getTimezoneOffset()
</script>
<script lang="ts">
import { ScheduleService } from '$lib/gen'
import { displayDate, emptyString, formatCron, sendUserToast } from '$lib/utils'
import CollapseLink from './CollapseLink.svelte'
import { emptyString, formatCron } from '$lib/utils'
import Badge from './Badge.svelte'
// @ts-ignore
import Multiselect from 'svelte-multiselect'
import TimezonePicker from 'svelte-timezone-picker'
import CollapseLink from './CollapseLink.svelte'
import { Button } from './common'
export let schedule: string
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
export let timezone: string // = Intl.DateTimeFormat().resolvedOptions().timeZone
export let disabled = false
export let validCRON = true
let preview: string[] = []
let cronError = ''
export let schedule: string = '0 0 12 * *'
export let disabled = false
let limit = 3
// If the user has already entered a cron string, switching to the basic tab will override it.
let executeEvery: 'second' | 'minute' | 'hour' | 'day-month' | 'month' | 'day-week' = 'minute'
$: !emptyString(schedule) && handleScheduleInput(schedule)
let seconds = 30
let minutes = 30
let hours = 1
const daysOfMonthOptions: number[] = Array.from(Array(31).keys()).map((i) => i + 1)
let daysOfMonth: number[] = []
// let lastDayOfMonth = false
const monthsOfYearOptions: string[] = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
let monthsOfYear: string[] = []
const daysOfWeekOptions: string[] = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
]
let daysOfWeek: string[] = []
let UTCTime: string = ''
async function handleScheduleInput(input: string): Promise<void> {
$: !emptyString(schedule) && handleScheduleInput(schedule, timezone)
async function handleScheduleInput(input: string, timezone: string): Promise<void> {
try {
preview = await ScheduleService.previewSchedule({
requestBody: { schedule: formatCron(input), offset: OFFSET }
requestBody: { schedule: formatCron(input), timezone }
})
cronError = ''
validCRON = true
} catch (err) {
if (err.status == 400 && err.body.includes('cron')) {
cronError = `Invalid cron expression`
validCRON = false
} else {
validCRON = false
}
}
}
let nschedule = ''
$: {
// CRON string format
// sec min hour day of month month day of week year
// 0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2
let s_daysOfMonth = ''
// if (lastDayOfMonth) {
// s_daysOfMonth = 'L'
// } else
if (daysOfMonth.length > 0) {
s_daysOfMonth = daysOfMonth.join(',')
} else {
s_daysOfMonth = '*'
}
let s_months = ''
if (monthsOfYear.length > 0) {
s_months = monthsOfYear.map((m) => m.slice(0, 3).toLowerCase()).join(',')
} else {
s_months = '*'
}
let s_daysOfWeek = ''
if (daysOfWeek.length > 0) {
s_daysOfWeek = daysOfWeek.map((d) => d.slice(0, 3).toLowerCase()).join(',')
} else {
s_daysOfWeek = '*'
}
const s_AtUTCHours = parseInt(UTCTime.split(':')[0]) || '0'
const s_AtUTCMinutes = parseInt(UTCTime.split(':')[1]) || '0'
// If using the basic editor, set the cron string based on the selected options
if (executeEvery === 'second') {
if (seconds > 0) {
nschedule = `*/${seconds} * * * *`
} else {
nschedule = `* * * * *`
}
} else if (executeEvery === 'minute') {
if (minutes > 0) {
nschedule = `0 */${minutes} * * *`
} else {
nschedule = `* * * * *`
}
} else if (executeEvery === 'hour') {
if (hours > 0) {
nschedule = `0 0 */${hours} * *`
} else {
nschedule = `* * * * *`
}
} else if (executeEvery === 'day-month') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} *`
} else if (executeEvery === 'month') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} ${s_daysOfMonth} ${s_months}`
} else if (executeEvery === 'day-week') {
nschedule = `0 ${s_AtUTCMinutes} ${s_AtUTCHours} * * ${s_daysOfWeek}`
}
}
$: dateFormatter = new Intl.DateTimeFormat('en-GB', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: timezone,
timeZoneName: 'short'
}).format
</script>
<div class="max-w-xl">
<div class="w-full text-right text-red-600 text-2xs grow">{cronError}</div>
<div class="flex flex-row items-end max-w-5xl">
<label class="text-xs min-w-max mr-2 self-center" for="cron-schedule">CRON expression</label>
<input
class="inline-block"
type="text"
id="cron-schedule"
name="cron-schedule"
bind:value={schedule}
{disabled}
/>
</div>
{#if !disabled}
<div class="flex flex-row text-xs text-blue-500 gap-3 pl-28 mb-2">
<button
on:click={() => {
schedule = '0 */15 * * *'
cronError = ''
}}>every 15 min</button
>
<button
on:click={() => {
schedule = '0 0 * * * *'
cronError = ''
}}>every hour</button
>
<button
on:click={() => {
schedule = '0 0 8 * * *'
cronError = ''
}}>once a day at 8AM</button
>
<div class="w-full flex space-x-16 p-4">
<div class="w-full flex flex-col space-y-2">
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Cron</small>
<input
class="inline-block"
type="text"
id="cron-schedule"
name="cron-schedule"
placeholder="*/30 * * * *"
bind:value={schedule}
{disabled}
/>
{#if !validCRON}
<small class="text-red-600"> Invalid cron syntax </small>
{/if}
</div>
{/if}
<CollapseLink text="preview next runs" open={true}>
{#if preview && preview.length > 0}
<div class="text-sm text-gray-700 border p-2 rounded-md">
<div class="flex flex-row justify-between">The next runs will be scheduled at:</div>
<ul class="list-disc mx-12">
{#each preview.slice(0, limit) as p}
<li class="mx-2 text-gray-700 text-sm">{displayDate(p, true)}</li>
{/each}
<li class="text-sm mx-2">...</li>
{#if limit != 10}
<button class="underline text-gray-400" on:click={() => (limit = 10)}>Load more</button>
{:else}
<button class="underline text-gray-400" on:click={() => (limit = 3)}>Load less</button>
{/if}
</ul>
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Timezone</small>
{#if disabled}
<div>
<Badge>{timezone}</Badge>
</div>
{:else}
<TimezonePicker {timezone} on:update={(e) => (timezone = e.detail.timezone)} />
{/if}
</div>
{#if !disabled}
<div class="w-full">
<CollapseLink text="Use simplified builder">
<div class="w-full flex flex-col gap-4 mt-4">
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Execute schedule every</small>
<div class="w-full flex gap-4">
<div class="w-full flex flex-col gap-1">
<select
{disabled}
name="execute_every"
id="execute_every"
bind:value={executeEvery}
>
<option value="second">Second(s)</option>
<option value="minute">Minute(s)</option>
<option value="hour">Hour(s)</option>
<option value="day-month">Day of the month</option>
<option value="month">Month(s)</option>
<option value="day-week">Day of the week</option>
</select>
</div>
<div class="w-full flex flex-col gap-1 justify-center">
{#if executeEvery == 'second'}
<input {disabled} type="number" min="0" max="59" bind:value={seconds} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'minute'}
<input {disabled} type="number" min="0" max="59" bind:value={minutes} />
<small>Valid range 0-59</small>
{:else if executeEvery == 'hour'}
<input {disabled} type="number" min="0" max="23" bind:value={hours} />
<small>Valid range 0-23</small>
{:else if executeEvery == 'day-month'}
<!-- <div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div> -->
{/if}
</div>
</div>
</div>
<div class="w-full flex flex-col gap-4">
{#if executeEvery == 'month'}
<div class="w-full flex flex-col">
<Multiselect
{disabled}
bind:selected={monthsOfYear}
options={monthsOfYearOptions}
selectedOptionsDraggable={false}
placeholder="Every month"
/>
</div>
{/if}
{#if executeEvery == 'day-week'}
<div class="w-full flex flex-col">
<Multiselect
{disabled}
bind:selected={daysOfWeek}
options={daysOfWeekOptions}
selectedOptionsDraggable={false}
placeholder="Every day"
/>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month'}
<div class="w-full flex flex-col gap-1">
{#if executeEvery == 'month'}
<small class="font-bold">On day of the month</small>
{/if}
<div class="w-full flex gap-4">
<div class="w-full flex">
<Multiselect
{disabled}
bind:selected={daysOfMonth}
options={daysOfMonthOptions}
selectedOptionsDraggable={false}
placeholder="Every day"
/>
</div>
<!-- {#if executeEvery == 'month'}
<div class="w-full flex">
<label for="lastDayOfMonth" class="w-full flex items-center gap-2">
<div class="flex">
<input type="checkbox" id="lastDayOfMonth" bind:checked={lastDayOfMonth} />
</div>
<small> Last day of the month </small>
</label>
</div>
{/if} -->
</div>
<small>Schedule will only execute on valid calendar days</small>
</div>
{/if}
{#if executeEvery == 'day-month' || executeEvery == 'month' || executeEvery == 'day-week'}
<div class="w-full flex flex-col gap-1">
<small class="font-bold">At UTC Time</small>
<input
{disabled}
type="time"
name="atUTCTime"
id="atUTCTime"
bind:value={UTCTime}
/>
</div>
{/if}
</div>
<div class="w-full flex flex-col gap-1">
<small class="font-bold">Preview New Cron</small>
<div class="flex p-2 px-4 rounded-md bg-gray-100">
<span>{nschedule}</span>
</div>
</div>
</div>
<div class="mt-4">
<Button variant="border" color="dark" size="xs" on:click={() => (schedule = nschedule)}
>Set Schedule</Button
>
</div>
</CollapseLink>
</div>
{/if}
</CollapseLink>
</div>
<div class="w-full flex flex-col space-y-2">
<h3>Execution summary</h3>
<hr />
<div class="flex flex-col space-y-2">
<small>Estimated upcoming events ({timezone})</small>
<div class="flex flex-col rounded-md p-4 border text-gray-600">
{#each preview as date}
<div class="flex items-center space-x-2">
<span>{dateFormatter(new Date(date))}</span>
</div>
{/each}
</div>
</div>
</div>
</div>

View File

@@ -99,19 +99,17 @@
<div class="mb-2 text-gray-500 text-sm bg-gray-50/20">
as JSON&nbsp;<input class="windmillapp" type="checkbox" bind:checked={forceJson} /></div
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
class="mb-2 min-w-[360px] text-sm text-gray-700 relative"
class="mb-2 w-full text-sm text-gray-700 relative"
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
<div class="text-gray-500 text-sm absolute top-0 right-2">
<div class="text-gray-500 text-sm absolute top-6 right-0">
<button on:click={jsonViewer.openDrawer}>Expand JSON</button>
</div></div
>{/if}{#if !forceJson && resultKind == 'table-col'}<div
class="grid grid-flow-col-dense border border-gray-200 rounded-md "
class="grid grid-flow-col-dense border border-gray-200 rounded-md"
>
{#each Object.keys(result) as col}
<div class="flex flex-col max-h-40 min-w-full">
<div
class="px-12 text-left uppercase border-b bg-gray-50 overflow-hidden rounded-t-md "
>
<div class="px-12 text-left uppercase border-b bg-gray-50 overflow-hidden rounded-t-md">
{col}
</div>
{#if Array.isArray(result[col])}
@@ -125,7 +123,7 @@
{/each}
</div>
{:else if !forceJson && resultKind == 'table-row'}<div
class="grid grid-flow-col-dense border border-gray-200 "
class="grid grid-flow-col-dense border border-gray-200"
>
<TableCustom>
<tbody slot="body">

View File

@@ -47,8 +47,8 @@
dispatch('click', { item: item?.eventName })
}
}}
class="block w-full whitespace-nowrap hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30
px-4 py-2 text-sm text-gray-700 text-left
class="block w-full whitespace-nowrap hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30
px-4 py-2 text-sm text-gray-700 text-left
{item.disabled ? 'bg-gray-200' : ''}
{item.separatorTop ? 'border-t' : ''} {item.separatorBottom ? 'border-b' : ''} {item.type ==
'delete'
@@ -72,7 +72,7 @@
<a
href={item.href}
on:click|stopPropagation|preventDefault={() => goto(item.href ?? '')}
class="block w-full px-4 font-semibold text-left py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30
class="block w-full px-4 font-semibold text-left py-2 text-sm text-gray-700 hover:drop-shadow-sm hover:bg-gray-50 hover:bg-opacity-30
{item.disabled ? 'bg-gray-200' : ''}"
role="menuitem"
tabindex="-1"
@@ -89,9 +89,10 @@
{item.displayName}
</a>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
class:bg-gray-50={item.disabled}
class="block text-left px-4 py-2 text-sm text-gray-700 cursor-auto"
class="block text-left px-4 py-2 text-sm text-gray-700 cursor-auto"
role="menuitem"
tabindex="-1"
id="user-menu-item-{name}-{i}}"

View File

@@ -1,10 +1,12 @@
<script lang="ts" context="module">
import getMessageServiceOverride from 'vscode/service-override/messages'
import getDialogServiceOverride from 'vscode/service-override/dialogs'
import getNotificationServiceOverride from 'vscode/service-override/notifications'
import { StandaloneServices } from 'vscode/services'
try {
StandaloneServices?.initialize({
...getMessageServiceOverride(document.body)
...getNotificationServiceOverride(document.body),
...getDialogServiceOverride()
})
} catch (e) {
console.error(e)
@@ -12,35 +14,34 @@
</script>
<script lang="ts">
import { browser, dev } from '$app/environment'
import { browser } from '$app/environment'
import { page } from '$app/stores'
import { sendUserToast } from '$lib/utils'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { buildWorkerDefinition } from 'monaco-editor-workers'
import type {
Disposable,
DocumentUri,
MessageTransports,
MonacoLanguageClient
} from 'monaco-languageclient'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { languages, editor as meditor, KeyCode, KeyMod, Uri as mUri } from 'monaco-editor'
languages.typescript.typescriptDefaults.setCompilerOptions({
target: languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
noLib: true
import 'monaco-editor/esm/vs/editor/edcore.main'
import {
editor as meditor,
KeyCode,
KeyMod,
Uri as mUri,
languages
} from 'monaco-editor/esm/vs/editor/editor.api'
import 'monaco-editor/esm/vs/basic-languages/python/python.contribution'
import 'monaco-editor/esm/vs/basic-languages/go/go.contribution'
import 'monaco-editor/esm/vs/basic-languages/shell/shell.contribution'
import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'
import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'
import { MonacoLanguageClient, MonacoServices } from 'monaco-languageclient'
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
import { CloseAction, ErrorAction, RequestType } from 'vscode-languageclient'
import * as vscode from 'vscode'
languages.typescript.typescriptDefaults.setModeConfiguration({
completionItems: false,
definitions: false,
hovers: false
})
languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSuggestionDiagnostics: true,
noSyntaxValidation: true
})
languages.typescript.typescriptDefaults.setExtraLibs([])
meditor.defineTheme('myTheme', {
base: 'vs',
@@ -53,32 +54,45 @@
})
meditor.setTheme('myTheme')
import {
BASH_INIT_CODE,
DENO_INIT_CODE_CLEAR,
GO_INIT_CODE,
PYTHON_INIT_CODE_CLEAR
} from '$lib/script_helpers'
import {
createHash as randomHash,
editorConfig,
langToExt,
updateOptions
} from '$lib/editorUtils'
import {
BASH_INIT_CODE,
DENO_INIT_CODE_CLEAR,
GO_INIT_CODE,
PYTHON_INIT_CODE_CLEAR
} from '$lib/script_helpers'
import type { Disposable } from 'vscode'
import type { DocumentUri, MessageTransports } from 'vscode-languageclient'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { buildWorkerDefinition } from './build_workers'
import { workspaceStore } from '$lib/stores'
import { UserService } from '$lib/gen'
let divEl: HTMLDivElement | null = null
let editor: meditor.IStandaloneCodeEditor
export let lang: 'typescript' | 'python' | 'go' | 'shell'
export let code: string = ''
export let hash: string = randomHash()
export let cmdEnterAction: (() => void) | undefined = undefined
export let formatAction: (() => void) | undefined = undefined
export let automaticLayout = true
export let websocketAlive = { pyright: false, black: false, deno: false, go: false }
export let shouldBindKey: boolean = true
export let fixedOverflowWidgets = true
export let path: string = randomHash()
if (path == '' || path == undefined || path.startsWith('/')) {
path = randomHash()
}
let initialPath: string = path
$: path != initialPath && handlePathChange()
let websockets: [MonacoLanguageClient, WebSocket][] = []
let websocketInterval: NodeJS.Timer | undefined
@@ -87,28 +101,12 @@
let disposeMethod: () => void | undefined
const dispatch = createEventDispatcher()
const uri = `file:///tmp/monaco/${hash}.${langToExt(lang)}`
const uri =
lang == 'go' ? `file:///tmp/monaco/${randomHash()}.go` : `file:///${path}.${langToExt(lang)}`
if (browser) {
if (dev) {
buildWorkerDefinition(
'../../../node_modules/monaco-editor-workers/dist/workers',
import.meta.url,
false
)
} else {
// @ts-ignore
self.MonacoEnvironment = {
getWorker: function (_moduleId: any, label: string) {
if (label === 'typescript' || label === 'javascript') {
return new tsWorker()
} else {
return new editorWorker()
}
}
}
}
}
// if (lang != 'typescript') {
buildWorkerDefinition('../../../workers', import.meta.url, false)
// }
export function getCode(): string {
return editor?.getValue() ?? ''
@@ -136,27 +134,31 @@
}
}
export function setCode(ncode: string): void {
export function setCode(ncode: string, noHistory: boolean = false): void {
code = ncode
if (editor?.getModel()) {
// editor.setValue(ncode)
editor.pushUndoStop()
if (noHistory) {
editor?.setValue(ncode)
} else {
if (editor?.getModel()) {
// editor.setValue(ncode)
editor.pushUndoStop()
editor.executeEdits('set', [
{
range: editor.getModel()!.getFullModelRange(), // full range
text: ncode
}
])
editor.executeEdits('set', [
{
range: editor.getModel()!.getFullModelRange(), // full range
text: ncode
}
])
editor.pushUndoStop()
editor.pushUndoStop()
}
}
}
export function format() {
if (editor) {
code = getCode()
editor.getAction('editor.action.formatDocument').run()
editor?.getAction('editor.action.formatDocument')?.run()
if (formatAction) {
formatAction()
}
@@ -182,22 +184,14 @@
export async function reloadWebsocket() {
await closeWebsockets()
const { MonacoLanguageClient } = await import('monaco-languageclient')
const { CloseAction, ErrorAction } = await import('vscode-languageclient')
const { toSocket, WebSocketMessageReader, WebSocketMessageWriter } = await import(
'vscode-ws-jsonrpc'
)
const vscode = await import('vscode')
const { RequestType } = await import('vscode-jsonrpc')
// install Monaco language client services
const { MonacoServices } = await import('monaco-languageclient')
monacoServices = MonacoServices.install()
function createLanguageClient(
transports: MessageTransports,
name: string,
initializationOptions?: any
initializationOptions: any,
middlewareOptions: ((params, token, next) => any) | undefined
) {
const client = new MonacoLanguageClient({
name: name,
@@ -216,13 +210,11 @@
initializationOptions,
middleware: {
workspace: {
configuration: (params, token, configuration) => {
return [
{
enable: true
}
]
}
configuration:
middlewareOptions ??
((params, token, next) => {
return [{ enabled: true }]
})
}
}
},
@@ -235,7 +227,12 @@
return client
}
async function connectToLanguageServer(url: string, name: string, options?: any) {
async function connectToLanguageServer(
url: string,
name: string,
initOptions: any,
middlewareOptions: any
) {
try {
const webSocket = new WebSocket(url)
@@ -243,7 +240,15 @@
const socket = toSocket(webSocket)
const reader = new WebSocketMessageReader(socket)
const writer = new WebSocketMessageWriter(socket)
const languageClient = createLanguageClient({ reader, writer }, name, options)
const languageClient = createLanguageClient(
{ reader, writer },
name,
initOptions,
middlewareOptions
)
// if (middlewareOptions != undefined) {
// languageClient.registerNotUsedFeatures()
// }
websockets.push([languageClient, webSocket])
// HACK ALERT: for some reasons, the client need to be restarted to take into account the 'go get <dep>' command
@@ -273,12 +278,13 @@
})
try {
console.log('started client')
console.log('starting client')
await languageClient.start()
console.log('started client')
} catch (err) {
console.log('err at client')
console.error(err)
throw new Error(err)
return
}
lastWsAttempt = new Date()
@@ -309,64 +315,138 @@
}
const wsProtocol = $page.url.protocol == 'https:' ? 'wss' : 'ws'
let encodedImportMap = ''
if (lang == 'typescript') {
await connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/deno`, 'deno', {
certificateStores: null,
enablePaths: [],
config: null,
importMap: null,
internalDebug: false,
lint: false,
path: null,
tlsCertificate: null,
unsafelyIgnoreCertificateErrors: null,
unstable: true,
enable: true,
cache: null,
codeLens: {
implementations: true,
references: true
},
suggest: {
autoImports: true,
completeFunctionCalls: false,
names: true,
paths: true,
if (path && path.split('/').length > 2) {
let expiration = new Date()
expiration.setHours(expiration.getHours() + 2)
const token = await UserService.createToken({
requestBody: { label: 'Ephemeral lsp token', expiration: expiration.toISOString() }
})
let root =
$page.url.protocol +
'//' +
$page.url.host +
'/api/scripts_u/tokened_raw/' +
$workspaceStore +
'/' +
token
const importMap = {
imports: {
autoDiscover: true,
hosts: {
'https://deno.land': true
'file:///': root + '/'
}
}
let path_splitted = path.split('/')
for (let c = 0; c < path_splitted.length; c++) {
let key = 'file://./'
for (let i = 0; i < c; i++) {
key += '../'
}
let url = path_splitted.slice(0, -c - 1).join('/')
let ending = c == path_splitted.length - 1 ? '' : '/'
importMap['imports'][key] = `${root}/${url}${ending}`
}
encodedImportMap = 'data:text/plain;base64,' + btoa(JSON.stringify(importMap))
}
await connectToLanguageServer(
`${wsProtocol}://${$page.url.host}/ws/deno`,
'deno',
{
certificateStores: null,
enablePaths: [],
config: null,
importMap: encodedImportMap,
internalDebug: false,
lint: false,
path: null,
tlsCertificate: null,
unsafelyIgnoreCertificateErrors: null,
unstable: true,
enable: true,
codeLens: {
implementations: true,
references: true,
referencesAllFunction: false
},
suggest: {
autoImports: true,
completeFunctionCalls: false,
names: true,
paths: true,
imports: {
autoDiscover: true,
hosts: {
'https://deno.land': true
}
}
}
},
() => {
return [
{
enable: true
}
]
}
})
)
} else if (lang === 'python') {
await connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/pyright`, 'pyright', {
executionEnvironments: [
{
root: '/tmp/pyright',
pythonVersion: '3.7',
pythonPlatform: 'platform',
extraPaths: []
await connectToLanguageServer(
`${wsProtocol}://${$page.url.host}/ws/pyright`,
'pyright',
{},
(params, token, next) => {
if (params.items.find((x) => x.section === 'python')) {
return [
{
analysis: {
useLibraryCodeForTypes: true,
autoImportCompletions: true,
diagnosticSeverityOverrides: { reportMissingImports: 'none' },
typeCheckingMode: 'basic'
}
}
]
}
]
})
if (params.items.find((x) => x.section === 'python.analysis')) {
return [
{
useLibraryCodeForTypes: true,
autoImportCompletions: true,
diagnosticSeverityOverrides: { reportMissingImports: 'none' },
typeCheckingMode: 'basic'
}
]
}
return next(params, token)
}
)
connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/black`, 'black', {
formatters: {
black: {
command: 'black',
args: ['--quiet', '-']
connectToLanguageServer(
`${wsProtocol}://${$page.url.host}/ws/black`,
'black',
{
formatters: {
black: {
command: 'black',
args: ['--quiet', '-']
}
},
formatFiletypes: {
python: 'black'
}
},
formatFiletypes: {
python: 'black'
}
})
undefined
)
} else if (lang === 'go') {
connectToLanguageServer(`${wsProtocol}://${$page.url.host}/ws/go`, 'go', {
'build.allowImplicitNetworkAccess': true
})
connectToLanguageServer(
`${wsProtocol}://${$page.url.host}/ws/go`,
'go',
{
'build.allowImplicitNetworkAccess': true
},
undefined
)
}
websocketInterval && clearInterval(websocketInterval)
@@ -397,6 +477,13 @@
}, 5000)
}
let pathTimeout: NodeJS.Timeout | undefined = undefined
function handlePathChange() {
initialPath = path
pathTimeout && clearTimeout(pathTimeout)
pathTimeout = setTimeout(reloadWebsocket, 3000)
}
async function closeWebsockets() {
command && command.dispose()
command = undefined
@@ -417,12 +504,14 @@
websocketInterval && clearInterval(websocketInterval)
}
let widgets: HTMLElement | undefined = document.getElementById('monaco-widgets-root') ?? undefined
async function loadMonaco() {
const model = meditor.createModel(code, lang, mUri.parse(uri))
model.updateOptions(updateOptions)
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(model, code, lang, automaticLayout, fixedOverflowWidgets),
overflowWidgetsDomNode: widgets,
tabSize: lang == 'python' ? 4 : 2
})
@@ -506,7 +595,7 @@
<div bind:this={divEl} class="{$$props.class} editor" />
<style>
<style lang="postcss">
.editor {
@apply p-0 border rounded-md border-gray-50;
}

View File

@@ -33,7 +33,7 @@
import Kbd from './common/kbd/Kbd.svelte'
export let lang: 'python3' | 'deno' | 'go' | 'bash'
export let editor: Editor
export let editor: Editor | undefined
export let websocketAlive: { pyright: boolean; black: boolean; deno: boolean; go: boolean }
export let iconOnly: boolean = false
export let validCode: boolean = true
@@ -50,10 +50,10 @@
undefined
function addEditorActions() {
editor.addAction('insert-variable', 'Windmill: Insert variable', () => {
editor?.addAction('insert-variable', 'Windmill: Insert variable', () => {
variablePicker.openDrawer()
})
editor.addAction('insert-resource', 'Windmill: Insert resource', () => {
editor?.addAction('insert-resource', 'Windmill: Insert resource', () => {
resourcePicker.openDrawer()
})
}
@@ -79,6 +79,8 @@
codeViewer?.openDrawer?.()
codeObj = await getScriptByPath(e.detail.path ?? '')
}
let version = __pkg__.version
</script>
<Drawer bind:this={scriptPicker} size="900px">
@@ -108,6 +110,7 @@
<ItemPicker
bind:this={contextualVariablePicker}
pickCallback={(path, name) => {
if (!editor) return
if (lang == 'deno') {
editor.insertAtCursor(`Deno.env.get('${name}')`)
} else if (lang == 'python3') {
@@ -133,10 +136,11 @@
<ItemPicker
bind:this={variablePicker}
pickCallback={(path, name) => {
if (!editor) return
if (lang == 'deno') {
if (!editor.getCode().includes('import * as wmill from')) {
editor.insertAtBeginning(
`import * as wmill from 'https://deno.land/x/windmill@v${__pkg__.version}/mod.ts'\n`
`import * as wmill from 'https://deno.land/x/windmill@v${version}/mod.ts'\n`
)
}
editor.insertAtCursor(`(await wmill.getVariable('${path}'))`)
@@ -180,10 +184,11 @@
<ItemPicker
bind:this={resourcePicker}
pickCallback={(path, _) => {
if (!editor) return
if (lang == 'deno') {
if (!editor.getCode().includes('import * as wmill from')) {
editor.insertAtBeginning(
`import * as wmill from 'https://deno.land/x/windmill@v${__pkg__.version}/mod.ts'\n`
`import * as wmill from 'https://deno.land/x/windmill@v${version}/mod.ts'\n`
)
}
editor.insertAtCursor(`(await wmill.getResource('${path}'))`)
@@ -233,7 +238,13 @@
{validCode ? 'Valid' : 'Invalid'}
</Badge>
<div class="flex items-center divide-x">
<Popover notClickable placement="bottom" disapperTimoout={0} class="pr-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="pr-1"
disablePopup={!iconOnly}
>
<Button
color="light"
btnClasses="!font-medium !h-full"
@@ -245,11 +256,15 @@
>
+Context Var
</Button>
<svelte:fragment slot="text">
Add context variable
</svelte:fragment>
<svelte:fragment slot="text">Add context variable</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
color="light"
btnClasses="!font-medium !h-full"
@@ -261,11 +276,15 @@
>
+Variable
</Button>
<svelte:fragment slot="text">
Add variable
</svelte:fragment>
<svelte:fragment slot="text">Add variable</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -277,33 +296,41 @@
>
+Resource
</Button>
<svelte:fragment slot="text">
Add resource
</svelte:fragment>
<svelte:fragment slot="text">Add resource</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor.clearContent}
on:click={editor?.clearContent}
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset
</Button>
<svelte:fragment slot="text">
Reset
</svelte:fragment>
<svelte:fragment slot="text">Reset</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
spacingSize="md"
color="light"
on:click={editor.reloadWebsocket}
on:click={editor?.reloadWebsocket}
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
@@ -320,17 +347,21 @@
{/if}
</span>
</Button>
<svelte:fragment slot="text">
Reload assistant
</svelte:fragment>
<svelte:fragment slot="text">Reload assistant</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor.format}
on:click={editor?.format}
{iconOnly}
startIcon={{ icon: faBroom }}
>
@@ -342,7 +373,13 @@
</Popover>
</div>
</div>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium"
size="xs"
@@ -352,15 +389,13 @@
{iconOnly}
startIcon={{ icon: faEye }}
>
Script
Explore other scripts
</Button>
<svelte:fragment slot="text">
Script
</svelte:fragment>
<svelte:fragment slot="text">Script</svelte:fragment>
</Popover>
</div>
<style>
<style lang="postcss">
span.green {
@apply text-green-600 animate-[pulse_5s_ease-in-out_infinite];
}

View File

@@ -4,15 +4,14 @@
import { initHistory, redo, undo } from '$lib/history'
import { userStore, workspaceStore } from '$lib/stores'
import { encodeState, formatCron, loadHubScripts, sendUserToast } from '$lib/utils'
import { faCalendarAlt, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { faCalendarAlt, faSave } from '@fortawesome/free-solid-svg-icons'
import { setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Button, ButtonPopup, ButtonPopupItem, UndoRedo } from './common'
import { Badge, Button, UndoRedo } from './common'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { OFFSET } from './CronInput.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import FlowEditor from './flows/FlowEditor.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import type { FlowState } from './flows/flowState'
import { dfs } from './flows/flowStore'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
@@ -20,6 +19,7 @@
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
import { Pen } from 'lucide-svelte'
export let initialPath: string = ''
export let selectedId: string | undefined
@@ -29,7 +29,7 @@
export let flowStateStore: Writable<FlowState>
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
const { cron, timezone, args, enabled } = $scheduleStore
try {
await ScheduleService.createSchedule({
@@ -37,7 +37,7 @@
requestBody: {
path: path,
schedule: formatCron(cron),
offset: OFFSET,
timezone,
script_path: path,
is_flow: true,
args,
@@ -53,73 +53,79 @@
async function saveFlow(leave: boolean): Promise<void> {
loadingSave = true
const flow = cleanInputs($flowStore)
const { cron, args, enabled } = $scheduleStore
$dirtyStore = false
if (initialPath === '') {
localStorage.removeItem('flow')
await FlowService.createFlow({
workspace: $workspaceStore!,
requestBody: {
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
try {
const flow = cleanInputs($flowStore)
const { cron, timezone, args, enabled } = $scheduleStore
$dirtyStore = false
if (initialPath === '') {
localStorage.removeItem('flow')
await FlowService.createFlow({
workspace: $workspaceStore!,
requestBody: {
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
}
})
if (enabled) {
await createSchedule(flow.path)
}
})
if (enabled) {
await createSchedule(flow.path)
}
} else {
localStorage.removeItem(`flow-${initialPath}`)
await FlowService.updateFlow({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
}
})
const scheduleExists = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path: flow.path
})
if (scheduleExists) {
const schedule = await ScheduleService.getSchedule({
} else {
localStorage.removeItem(`flow-${initialPath}`)
await FlowService.updateFlow({
workspace: $workspaceStore!,
path: initialPath,
requestBody: {
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
}
})
const scheduleExists = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path: flow.path
})
if (JSON.stringify(schedule.args) != JSON.stringify(args) || schedule.schedule != cron) {
await ScheduleService.updateSchedule({
if (scheduleExists) {
const schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path: flow.path,
requestBody: {
schedule: formatCron(cron),
args
}
path: flow.path
})
if (JSON.stringify(schedule.args) != JSON.stringify(args) || schedule.schedule != cron) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore ?? '',
path: flow.path,
requestBody: {
schedule: formatCron(cron),
timezone,
args
}
})
}
if (enabled != schedule.enabled) {
await ScheduleService.setScheduleEnabled({
workspace: $workspaceStore ?? '',
path: flow.path,
requestBody: { enabled }
})
}
} else if (enabled) {
await createSchedule(flow.path)
}
if (enabled != schedule.enabled) {
await ScheduleService.setScheduleEnabled({
workspace: $workspaceStore ?? '',
path: flow.path,
requestBody: { enabled }
})
}
} else if (enabled) {
await createSchedule(flow.path)
}
}
loadingSave = false
if (leave) {
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
} else if (initialPath !== $flowStore.path) {
initialPath = $flowStore.path
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
loadingSave = false
if (leave) {
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
} else if (initialPath !== $flowStore.path) {
initialPath = $flowStore.path
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
}
} catch (err) {
sendUserToast(`The flow could not be saved: ${err.body}`, true)
loadingSave = false
}
}
@@ -150,7 +156,12 @@
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
const scheduleStore = writable<Schedule>({
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)
@@ -182,6 +193,7 @@
.catch(() => {
scheduleStore.set({
cron: '0 */5 * * *',
timezone: 'UTC',
args: {},
enabled: false
})
@@ -251,6 +263,23 @@
...dfs($flowStore.value.modules, (module) => module.id)
]
}
const dropdownItems: Array<{
label: string
onClick: () => void
}> = [
{
label: 'Save and exit',
onClick: () => saveFlow(true)
}
]
if (initialPath != '') {
dropdownItems.push({
label: 'Fork',
onClick: () => window.open(`/flows/add?template=${initialPath}`)
})
}
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -261,10 +290,17 @@
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden max-h-12 h-full"
>
<div class="flex flex-row gap-4 items-center">
<FlowImportExportMenu />
<div class="flex w-full max-w-md gap-4 items-center">
<div class="min-w-64 w-full">
<input
type="text"
placeholder="Flow summary"
class="text-sm w-full font-semibold"
bind:value={$flowStore.summary}
/>
</div>
<UndoRedo
undoProps={{ disabled: $history.index === 0 }}
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
@@ -278,7 +314,7 @@
/>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
{#if $scheduleStore.enabled}
<Button
btnClasses="hidden lg:inline-flex"
@@ -293,53 +329,47 @@
{$scheduleStore.cron ?? ''}
</Button>
{/if}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings-metadata')
document.getElementById('path')?.focus()
}}
>
{$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
</Button>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings-metadata')
document.getElementById('flow-summary')?.focus()
}}
>
<div class="max-w-[10em] !truncate">
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
<div class="flex justify-start w-full">
<div>
<button
on:click={async () => {
select('settings-metadata')
document.getElementById('path')?.focus()
}}
>
<Badge
color="gray"
class="center-center !bg-gray-300 !text-gray-600 !h-[28px] !w-[70px] rounded-r-none"
>
<Pen size={12} class="mr-2" /> Path
</Badge>
</button>
</div>
</Button>
<input
type="text"
readonly
value={$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
currentTarget.select()
}}
/>
</div>
</div>
<div class="flex flex-row space-x-2">
<FlowImportExportMenu />
<FlowPreviewButtons />
<div class="center-center">
<ButtonPopup
<Button
loading={loadingSave}
size="sm"
size="xs"
startIcon={{ icon: faSave }}
on:click={() => saveFlow(false)}
{dropdownItems}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => saveFlow(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/flows/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
Save
</Button>
</div>
</div>
</div>

View File

@@ -20,10 +20,10 @@
}
export let overflowAuto = false
export let noSide = false
export let download = false
let stepDetail: FlowModule | string | undefined = undefined
let codeViewer: Drawer
let topHeight = 0
</script>
<Drawer bind:this={codeViewer} size="900px">
@@ -48,7 +48,7 @@
<div class="mt-6">
<h3>Code</h3>
<iframe
class="w-full h-full text-sm"
class="w-full h-full text-sm"
title="embedded script from hub"
frameborder="0"
src="https://hub.windmill.dev/embed/script/{stepDetail.value?.path?.substring(4)}"
@@ -77,6 +77,7 @@
class:overflow-auto={overflowAuto}
>
<FlowGraph
{download}
minHeight={400}
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
@@ -141,7 +142,7 @@
>
</h3>
<iframe
class="w-full h-full text-sm"
class="w-full h-full text-sm"
title="embedded script from hub"
frameborder="0"
src="https://hub.windmill.dev/embed/script/{stepDetail.value?.path?.substring(4)}"
@@ -154,11 +155,7 @@
json={stepDetail.value.iterator.value}
/>{:else}
<span class="text-xs">
<Highlight
offsetTop={0}
language={typescript}
code={cleanExpr(stepDetail.value.iterator.expr)}
/>
<Highlight language={typescript} code={cleanExpr(stepDetail.value.iterator.expr)} />
</span>
{/if}</p
>

View File

@@ -95,12 +95,14 @@
<div class="flex divide-y flex-col space-y-2 h-screen bg-white px-6 py-2 w-full">
<div class="flex flex-row justify-between w-full items-center gap-x-2">
<button
on:click={() => dispatch('close')}
class="hover:bg-gray-200 bg-gray-100 rounded-full w-16 h-8 flex items-center justify-center transition-all"
>
<Icon data={faClose} class="text-gray-500" />
</button>
<div class="w-8">
<button
on:click={() => dispatch('close')}
class="hover:bg-gray-200 bg-gray-100 rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<Icon data={faClose} class="text-gray-500" />
</button>
</div>
{#if isRunning}
<Button
@@ -136,17 +138,17 @@
</Button>
{/if}
<Button
btnClasses="h-full"
btnClasses="h-full truncate"
size="sm"
variant="border"
on:click={() => {
capturePayload.openDrawer()
}}>Fill test args from a request</Button
}}>Fill args from a request</Button
>
</div>
<FlowProgressBar {job} bind:reset={jobProgressReset} />
<div class="overflow-y-auto grow divide-y divide-gray-600 pr-4">
<div class="overflow-y-auto grow divide-y divide-gray-600 pr-4">
<div class="max-h-1/2 overflow-auto border-b border-gray-700">
<SchemaForm
noVariablePicker

View File

@@ -173,6 +173,34 @@
return arg == true
}
}
function onJobsLoaded(mod: FlowStatusModule, job: Job): void {
if (mod.id && (mod.flow_jobs ?? []).length == 0) {
if (flowState && flowState[mod.id]) {
flowState[mod.id].previewResult = job['result']
flowState[mod.id].previewArgs = job.args
}
if (job.type == 'QueuedJob') {
localFlowModuleStates[mod.id] = {
type: FlowStatusModule.type.IN_PROGRESS,
logs: job.logs,
args: job.args,
parent_module: mod['parent_module']
}
} else {
localFlowModuleStates[mod.id] = {
args: job.args,
type: job['success'] ? FlowStatusModule.type.SUCCESS : FlowStatusModule.type.FAILURE,
logs: job.logs,
result: job['result'],
job_id: job.id,
parent_module: mod['parent_module'],
iteration_total: mod.iterator?.itered?.length
// retries: flowState?.raw_flow
}
}
}
}
</script>
{#if job}
@@ -379,49 +407,31 @@
<div class="line w-8 h-10" />
<li class="w-full border border-gray-600 p-6 space-y-2 bg-blue-50/50">
{#if [FlowStatusModule.type.IN_PROGRESS, FlowStatusModule.type.SUCCESS, FlowStatusModule.type.FAILURE].includes(mod.type)}
<svelte:self
{workspaceId}
bind:suspend_status
bind:retry_status
bind:flowState
bind:flowModuleStates={localFlowModuleStates}
jobId={mod.job}
flowJobIds={mod.flow_jobs
? {
moduleId: mod.id,
flowJobs: mod.flow_jobs
}
: undefined}
on:jobsLoaded={(e) => {
if (mod.id && (mod.flow_jobs ?? []).length == 0) {
if (flowState && flowState[mod.id]) {
flowState[mod.id].previewResult = e.detail.result
flowState[mod.id].previewArgs = e.detail.args
}
if (e.detail.type == 'QueuedJob') {
localFlowModuleStates[mod.id] = {
type: FlowStatusModule.type.IN_PROGRESS,
logs: e.detail.logs,
args: e.detail.args,
parent_module: mod['parent_module']
}
} else {
localFlowModuleStates[mod.id] = {
args: e.detail.args,
type: e.detail.success
? FlowStatusModule.type.SUCCESS
: FlowStatusModule.type.FAILURE,
logs: e.detail.logs,
result: e.detail.result,
job_id: e.detail.id,
parent_module: mod['parent_module'],
iteration_total: mod.iterator?.itered?.length
// retries: flowState?.raw_flow
}
}
}
}}
/>
{#if job.raw_flow?.modules[i]?.value.type == 'flow'}
<svelte:self
{workspaceId}
jobId={mod.job}
bind:suspend_status
bind:retry_status
on:jobsLoaded={(e) => onJobsLoaded(mod, e.detail)}
/>
{:else}
<svelte:self
{workspaceId}
bind:suspend_status
bind:retry_status
bind:flowState
bind:flowModuleStates={localFlowModuleStates}
jobId={mod.job}
flowJobIds={mod.flow_jobs
? {
moduleId: mod.id,
flowJobs: mod.flow_jobs
}
: undefined}
on:jobsLoaded={(e) => onJobsLoaded(mod, e.detail)}
/>
{/if}
{:else}
<ModuleStatus
type={mod.type}
@@ -492,7 +502,7 @@
<p class="p-2">No arguments</p>
{/if}
{:else if node}
<div class="px-2 flex gap-2 min-w-0 ">
<div class="px-2 flex gap-2 min-w-0">
<ModuleStatus type={node.type} scheduled_for={node['scheduled_for']} />
{#if node.job_id}
<div class="truncate"

View File

@@ -75,7 +75,7 @@
{:else}
<div class="text-gray-700 text-xs italic mb-4">No inputs</div>
{/if}
<FlowGraphViewer {noSide} {flow} overflowAuto />
<FlowGraphViewer download {noSide} {flow} overflowAuto />
</div>
</TabContent>
<TabContent value="json">

View File

@@ -19,11 +19,9 @@
type Role = 'viewer' | 'writer' | 'admin'
let folder: Folder | undefined
let perms: { owner_name: string; role: Role }[] | undefined = undefined
let managing_folders: string[] = []
let usernames: string[] = []
let groups: string[] = []
let ownerItem: string = ''
let folders: string[] = []
async function loadUsernames(): Promise<void> {
usernames = await UserService.listUsernames({ workspace: $workspaceStore! })
@@ -42,14 +40,9 @@
async function load() {
loadUsernames()
loadGroups()
loadFolders()
await loadFolder()
}
async function loadFolders(): Promise<void> {
folders = (await FolderService.listFolders({ workspace: $workspaceStore! })).map((x) => x.name)
}
async function addToFolder() {
await GranularAclService.addGranularAcls({
workspace: $workspaceStore ?? '',
@@ -67,7 +60,8 @@
can_write =
$userStore != undefined &&
(folder?.owners.includes('u/' + $userStore.username) ||
($userStore.is_admin ?? false) || ($userStore.is_super_admin ?? false) ||
($userStore.is_admin ?? false) ||
($userStore.is_super_admin ?? false) ||
$userStore.pgroups.findIndex((x) => folder?.owners.includes(x)) != -1)
perms = Array.from(
@@ -82,9 +76,6 @@
role: getRole(x)
}
})
managing_folders = Object.entries(folder?.extra_perms ?? {})
.filter(([k, v]) => k.startsWith('g/') && v)
.map(([k, v]) => k)
}
function getRole(x: string): Role {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { FolderService, GroupService } from '$lib/gen'
import { FolderService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
export let name: string

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { GranularAclService, GroupService, UserService, type Group } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { type Group, GroupService, UserService, GranularAclService } from '$lib/gen'
import AutoComplete from 'simple-svelte-autocomplete'
import TableCustom from './TableCustom.svelte'
import { canWrite, sendUserToast } from '$lib/utils'
import AutoComplete from 'simple-svelte-autocomplete'
import { createEventDispatcher } from 'svelte'
import autosize from 'svelte-autosize'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import Skeleton from './common/skeleton/Skeleton.svelte'
import TableCustom from './TableCustom.svelte'
import Tooltip from './Tooltip.svelte'
import autosize from 'svelte-autosize'
import { createEventDispatcher } from 'svelte'
export let name: string
let can_write = false
@@ -16,10 +16,8 @@
type Role = 'member' | 'manager' | 'admin'
let group: Group | undefined
let members: { member_name: string; role: Role }[] | undefined = undefined
let managing_groups: string[] = []
let usernames: string[] | undefined = []
let username: string = ''
let groups: string[] = []
const dispatch = createEventDispatcher()
@@ -34,15 +32,10 @@
}
async function load() {
loadGroups()
await loadGroup()
loadUsernames()
}
async function loadGroups(): Promise<void> {
groups = (await GroupService.listGroups({ workspace: $workspaceStore! })).map((x) => x.name)
}
async function addToGroup() {
await GroupService.addUserToGroup({
workspace: $workspaceStore ?? '',
@@ -68,9 +61,6 @@
role: getRole(x)
}
})
managing_groups = Object.entries(group?.extra_perms ?? {})
.filter(([k, v]) => k.startsWith('g/') && v)
.map(([k, v]) => k)
}
function getRole(x: string): Role {

View File

@@ -27,7 +27,7 @@
</script>
{#if code?.length < 5000}
<Highlight className="nowrap {$$props.class}" language={lang} {code} />
<Highlight class="nowrap {$$props.class}" language={lang} {code} />
{:else}
<pre class="overflow-auto max-h-screen {$$props.class}"
><code class="language-{language}">{code}</code></pre

View File

@@ -6,7 +6,8 @@
export let content: string
</script>
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap"
<!-- svelte-ignore a11y-click-events-have-key-events -->
<pre class="bg-gray-700 text-gray-100 p-2 font-mono text-sm whitespace-pre-wrap"
>{content} <span on:click={() => copyToClipboard(content)} class="cursor-pointer ml-2"
><Icon data={faClipboard} /></span
></pre

View File

@@ -15,7 +15,6 @@
import type ItemPicker from './ItemPicker.svelte'
import type { InputTransform } from '$lib/gen'
import TemplateEditor from './TemplateEditor.svelte'
import Tooltip from './Tooltip.svelte'
import { setInputCat as computeInputCat } from '$lib/utils'
export let schema: Schema
@@ -40,17 +39,24 @@
schema.properties[argName].enum,
schema.properties[argName].contentEncoding
)
let propertyType = getPropertyType(arg)
function getPropertyType(arg: InputTransform | any): 'static' | 'javascript' {
let type: 'static' | 'javascript' = arg?.type ?? 'static'
if (type == 'javascript') {
if (
type == 'javascript' &&
isStaticTemplate(inputCat) &&
(arg?.expr?.length === 0 || arg?.expr?.[0] === '`')
) {
const newValue = codeToStaticTemplate(arg.expr)
if (newValue) {
type = 'static'
arg.value = newValue
}
}
return type
}
@@ -112,8 +118,6 @@
const { focusProp, propPickerConfig } = getContext<PropPickerWrapperContext>('PropPickerWrapper')
$: isStaticTemplate(inputCat) && propertyType == 'static' && setPropertyType(arg?.value)
const openBracket = '${'
const closeBracket = '}'
function setDefaultCode() {
if (!arg?.value) {
@@ -148,86 +152,89 @@
</div>
{#if !noDynamicToggle}
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap z-10">
<ToggleButtonGroup
bind:selected={propertyType}
on:selected={(e) => {
const staticTemplate = isStaticTemplate(inputCat)
if (e.detail === 'javascript') {
if (arg.expr == undefined) {
arg.expr = getDefaultExpr(
argName,
previousModuleId,
staticTemplate
? `\`${arg?.value?.toString().replaceAll('`', '\\`') ?? ''}\``
: arg.value
? JSON.stringify(arg?.value, null, 4)
: ''
)
}
if (arg) {
arg.value = undefined
}
propertyType = 'javascript'
arg.type = 'javascript'
} else {
if (staticTemplate) {
if (arg) {
arg.value = codeToStaticTemplate(arg.expr)
arg.expr = undefined
<div>
<ToggleButtonGroup
bind:selected={propertyType}
on:selected={(e) => {
const staticTemplate = isStaticTemplate(inputCat)
if (e.detail === 'javascript') {
if (arg.expr == undefined) {
arg.expr = getDefaultExpr(
argName,
previousModuleId,
staticTemplate
? `\`${arg?.value?.toString().replaceAll('`', '\\`') ?? ''}\``
: arg.value
? JSON.stringify(arg?.value, null, 4)
: ''
)
}
setPropertyType(arg?.value)
} else {
if (arg) {
arg.type = 'static'
arg.value = undefined
arg.expr = undefined
arg.type = 'javascript'
}
propertyType = 'javascript'
} else {
if (staticTemplate) {
if (arg) {
arg.value = codeToStaticTemplate(arg.expr)
arg.expr = undefined
}
setPropertyType(arg?.value)
} else {
if (arg) {
arg.type = 'static'
arg.value = undefined
arg.expr = undefined
}
}
propertyType = 'static'
}
propertyType = 'static'
}
}}
>
{#if isStaticTemplate(inputCat)}
<ToggleButton light position="left" value="static" size="xs">
{'${} '}&nbsp;
<Tooltip
>Write text or surround javascript with "{openBracket}" and "{closeBracket}". Use
`result` to connect to another node's output.
</Tooltip></ToggleButton
>
{:else}
<ToggleButton light position="left" value="static" size="xs">Static</ToggleButton>
{/if}
<ToggleButton
light
position="right"
value="javascript"
startIcon={{ icon: faCode }}
size="xs"
>&nbsp;<Tooltip
>Write javascript expressions directly, using 'flow_input' or 'result'. You can use
multiline javascript.
</Tooltip></ToggleButton
}}
>
</ToggleButtonGroup>
<Button
variant="contained"
color="blue"
size="xs"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return true
})
}}>Connect &rightarrow;</Button
>
{#if isStaticTemplate(inputCat)}
<ToggleButton
title={`Write text or surround javascript with \`\$\{\` and \`\}\`. Use \`result\` to connect to another node\'s output.`}
light
position="left"
value="static"
size="xs2"
>
{'${} '}&nbsp;
</ToggleButton>
{:else}
<ToggleButton light position="left" value="static" size="xs2">Static</ToggleButton>
{/if}
<ToggleButton
light
title="Write javascript expressions directly, using 'flow_input' or 'result'. You can use multiline javascript."
position="right"
value="javascript"
startIcon={{ icon: faCode }}
size="xs2"
/>
</ToggleButtonGroup>
</div>
<div>
<Button
title="Connect to another node's output"
variant="border"
color="dark"
size="xs2"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return true
})
}}>Link &rightarrow;</Button
>
</div>
</div>
{/if}
</div>
<div class="max-w-xs" />
<div
class="relative mb-6 {$propPickerConfig?.propName == argName
? 'outline outline-offset-0 outline-2 outline-blue-500 rounded-md'
@@ -241,7 +248,7 @@
</span>
{/if}
{#if isStaticTemplate(inputCat) && propertyType == 'static' && !noDynamicToggle}
<div class="py-1 mt-2 min-h-[28px] rounded border border-1 border-gray-500">
<div class="mt-2 min-h-[28px] rounded border border-1 border-gray-500">
{#if arg}
<TemplateEditor
bind:this={monacoTemplate}

View File

@@ -55,7 +55,7 @@
<div class="w-full {clazz}">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#each keys as argName (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="z-10">
<InputTransformForm

View File

@@ -15,7 +15,7 @@
json={val.value}
/>{:else}
<span class="text-xs">
<Highlight offsetTop={0} language={typescript} code={cleanExpr(val.expr)} />
<Highlight language={typescript} code={cleanExpr(val.expr)} />
</span>
{/if}
</li>

View File

@@ -314,5 +314,14 @@
{/if}
<slot name="actions" />
</div>
{#if error && error != ''}
<div class="text-right text-xs text-red-600">
{#if error === ''}
&nbsp;
{:else}
{error}
{/if}
</div>
{/if}
</div>
</div>

View File

@@ -11,7 +11,7 @@
</script>
<div class="w-full">
{#each Object.keys(schema.properties ?? {}) as argName, i (argName)}
{#each Object.keys(schema.properties ?? {}) as argName (argName)}
<div>
{#if typeof args == 'object' && schema?.properties[argName] && args}
<LightweightArgInput

View File

@@ -65,11 +65,11 @@
<p class="font-semibold text-gray-800"><slot name="title" /></p>
<div />
</div>
<div class="flex flex-col bg-gray-50 pt-3 px-6 grow overflow-y-auto">
<div class="flex flex-col bg-gray-50 pt-3 px-6 grow overflow-y-auto">
<slot name="content" />
</div>
<div class="flex flex-col bg-white border-gray-200 p-2">
<div class="flex flex-row justify-between p-2 ">
<div class="flex flex-row justify-between p-2">
<button
on:click={() => {
closeDrawer()
@@ -98,7 +98,7 @@
</div>
{/if}
<style>
<style lang="postcss">
.blurred-background {
/* @apply absolute sm:top-6 lg:top-8 left-28 sm:left-40 md:left-48; */ /* If we wanted to make the navbars visible */
@apply fixed top-0 left-0;

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type InputTransform, type Job } from '$lib/gen'
import { getScriptByPath, sendUserToast, truncateRev } from '$lib/utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import RunForm from './RunForm.svelte'
import TestJobLoader from './TestJobLoader.svelte'
import LogViewer from './LogViewer.svelte'
import DisplayResult from './DisplayResult.svelte'
import Button from './common/button/Button.svelte'
import { workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
import { getContext } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import Button from './common/button/Button.svelte'
import DisplayResult from './DisplayResult.svelte'
import type { FlowEditorContext } from './flows/types'
import LogViewer from './LogViewer.svelte'
import RunForm from './RunForm.svelte'
import TestJobLoader from './TestJobLoader.svelte'
export let mod: FlowModule
export let schema: Schema
@@ -43,14 +43,14 @@
export async function runTest(args: any) {
const val = mod.value
let jobId: string | undefined = undefined
// let jobId: string | undefined = undefined
if (val.type == 'rawscript') {
jobId = await testJobLoader?.runPreview(val.path, val.content, val.language, args)
await testJobLoader?.runPreview(val.path, val.content, val.language, args)
} else if (val.type == 'script') {
const script = val.hash
? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash: val.hash })
: await getScriptByPath(val.path)
jobId = await testJobLoader?.runPreview(val.path, script.content, script.language, args)
await testJobLoader?.runPreview(val.path, script.content, script.language, args)
} else {
throw Error('not testable module type')
}

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { FlowStatusModule } from '$lib/gen'
import { displayDate } from '$lib/utils'
import { faHourglassHalf } from '@fortawesome/free-solid-svg-icons'
import { Icon } from 'svelte-awesome'
import { Badge } from './common'

View File

@@ -110,6 +110,7 @@
</script>
<div class="multiselect" class:readonly>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="tokens" class:showOptions on:click={handleTokenClick}>
{#each Object.values(selected) as s}
<div class="token" data-id={s.value}>

View File

@@ -1,7 +1,4 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import ResourcePicker from './ResourcePicker.svelte'
export let format: string
@@ -12,26 +9,6 @@
}
let path: string = ''
let args: Record<string, any> = {}
let schema: any | undefined = undefined
let resourceTypeName: string = ''
$: format.startsWith('resource-') && loadSchema(format)
async function loadSchema(format: string) {
resourceTypeName = format.substring('resource-'.length)
try {
schema = (
await ResourceService.getResourceType({
workspace: $workspaceStore!,
path: resourceTypeName
})
).schema
} catch (e) {
schema = undefined
}
}
function resourceToValue() {
if (path) {
@@ -46,10 +23,6 @@
}
function valueToPath() {
if (!isString(value) && value) {
args = value
}
if (isResource()) {
path = value.substr('$res:'.length)
}
@@ -60,7 +33,6 @@
<div class="flex flex-row w-full flex-wrap gap-x-2 gap-y-0.5">
<ResourcePicker
on:refresh={() => loadSchema(format)}
on:change={(e) => {
path = e.detail
resourceToValue()

View File

@@ -1,29 +1,32 @@
<script lang="ts" context="module">
const lastMetaUsed = writable<Meta | undefined>(undefined)
</script>
<script lang="ts">
import { type Meta, pathToMeta } from '$lib/common'
import { pathToMeta, type Meta } from '$lib/common'
import {
AppService,
FlowService,
FolderService,
GroupService,
ResourceService,
ScheduleService,
ScriptService,
VariableService
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import Required from './Required.svelte'
import { Button, Drawer, DrawerContent } from './common'
import { faEye, faPlus } from '@fortawesome/free-solid-svg-icons'
import ToggleButtonGroup from './common/toggleButton/ToggleButtonGroup.svelte'
import ToggleButton from './common/toggleButton/ToggleButton.svelte'
import { createEventDispatcher } from 'svelte'
import { Icon } from 'svelte-awesome'
import Tooltip from './Tooltip.svelte'
import FolderEditor from './FolderEditor.svelte'
import GroupEditor from './GroupEditor.svelte'
import { random_adj } from './random_positive_adjetive'
import { writable } from 'svelte/store'
import { Button, Drawer, DrawerContent } from './common'
import Badge from './common/badge/Badge.svelte'
import ToggleButton from './common/toggleButton/ToggleButton.svelte'
import ToggleButtonGroup from './common/toggleButton/ToggleButtonGroup.svelte'
import FolderEditor from './FolderEditor.svelte'
import { random_adj } from './random_positive_adjetive'
import Required from './Required.svelte'
import Tooltip from './Tooltip.svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
let meta: Meta | undefined = undefined
@@ -40,8 +43,7 @@
const dispatch = createEventDispatcher()
let folders: string[] = []
let groups: string[] = []
let folders: { name: string; write: boolean }[] = []
$: meta && onMetaChange()
@@ -49,6 +51,10 @@
if (meta) {
path = metaToPath(meta)
validate(meta, path, kind)
$lastMetaUsed = {
...meta,
name: ''
}
}
}
@@ -71,14 +77,17 @@
export async function reset() {
if (path == '' || path == 'u//') {
meta = {
ownerKind: 'user',
name: random_adj() + '_' + namePlaceholder,
owner: ''
if ($lastMetaUsed == undefined) {
meta = {
ownerKind: 'user',
name: random_adj() + '_' + namePlaceholder,
owner: ''
}
meta.owner = $userStore!.username.split('@')[0].replace(/[^a-zA-Z0-9]/g, '')
} else {
meta = { ...$lastMetaUsed, name: random_adj() + '_' + namePlaceholder }
}
meta.owner = $userStore!.username.split('@')[0]
let newMeta = { ...meta }
while (await pathExists(metaToPath(newMeta), kind)) {
disabled = true
@@ -95,30 +104,26 @@
}
async function loadFolders(): Promise<void> {
let initialFolders: string[] = []
let initialFolders: { name: string; write: boolean }[] = []
let initialFolder = ''
if (initialPath?.split('/')?.[0] == 'f') {
initialFolder = initialPath?.split('/')?.[1]
initialFolders.push(initialFolder)
initialFolders.push({ name: initialFolder, write: true })
}
folders = initialFolders.concat(
(
await FolderService.listFolderNames({
workspace: $workspaceStore!
})
).filter((x) => x != initialFolder)
)
}
async function loadGroups(): Promise<void> {
let initialGroups: string[] = []
if (initialPath?.split('/')?.[0] == 'f') {
initialGroups.push(initialPath?.split('/')?.[1])
}
groups = initialGroups.concat(
await GroupService.listGroupNames({
workspace: $workspaceStore!
})
)
.filter((x) => x != initialFolder)
.map((x) => ({
name: x,
write:
($userStore?.folders?.includes(x) == true ?? false) ||
($userStore?.is_admin ?? false) ||
($userStore?.is_super_admin ?? false)
}))
)
}
@@ -194,7 +199,6 @@
$: {
if ($workspaceStore && $userStore) {
loadFolders()
loadGroups()
initPath()
}
}
@@ -224,56 +228,14 @@
requestBody: { name: newFolderName }
})
folderCreated = newFolderName
$userStore?.folders?.push(newFolderName)
loadFolders()
if (meta) {
meta.owner = newFolderName
}
loadFolders()
}
let newGroup: Drawer
let viewGroup: Drawer
let newGroupName: string
let groupCreated: string | undefined = undefined
async function addGroup() {
await GroupService.createGroup({
workspace: $workspaceStore ?? '',
requestBody: { name: newGroupName }
})
groupCreated = newGroupName
if (meta) {
meta.owner = newGroupName
}
loadGroups()
}
</script>
<Drawer bind:this={newGroup}>
<DrawerContent
title="New Folder"
on:close={() => {
newGroup.closeDrawer()
groupCreated = undefined
}}
>
<div class="flex flex-row">
<input class="mr-2" placeholder="New group name" bind:value={newGroupName} />
<Button size="md" endIcon={{ icon: faPlus }} disabled={!newGroupName} on:click={addGroup}>
New&nbsp;group
</Button>
</div>
{#if groupCreated}
<div class="mt-8" />
<GroupEditor name={groupCreated} />
{/if}
</DrawerContent>
</Drawer>
<Drawer bind:this={viewGroup}>
<DrawerContent title="Folder {meta?.owner}" on:close={viewGroup.closeDrawer}>
<GroupEditor name={meta?.owner ?? ''} />
</DrawerContent>
</Drawer>
<Drawer bind:this={newFolder}>
<DrawerContent
title="New Folder"
@@ -310,6 +272,7 @@
<div class="flex flex-col sm:flex-row items-center gap-2 sm:gap-4 pb-0 mb-1">
{#if meta != undefined}
<div class="flex gap-4 shrink">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
@@ -356,8 +319,8 @@
<div class="flex flex-row gap-1 w-full">
<select class="grow w-full" {disabled} bind:value={meta.owner}>
{#each folders as f}
<option>{f}</option>
{#each folders as { name, write }}
<option disabled={!write}>{name}{write ? '' : ' (read-only)'}</option>
{/each}
</select>
<Button variant="border" size="xs" on:click={viewFolder.openDrawer}>
@@ -368,27 +331,6 @@
></div
>
</label>
{:else if meta.ownerKind === 'group'}
<label class="block grow w-48">
<span class="text-gray-700 text-sm"
>Group <Tooltip>Item will be owned by the group and hence all its member</Tooltip
></span
>
<div class="flex flex-row gap-1">
<select class="grow w-full" {disabled} bind:value={meta.owner}>
{#each groups as g}
<option>{g}</option>
{/each}
</select>
<Button variant="border" size="xs" on:click={viewGroup.openDrawer}>
<Icon scale={0.8} data={faEye} /></Button
>
<Button variant="border" size="xs" on:click={newGroup.openDrawer}>
<Icon scale={0.8} data={faPlus} /></Button
></div
>
</label>
{/if}
</div>
<label class="block grow w-full max-w-md">
@@ -396,6 +338,7 @@
Name
<Required required={true} />
</span>
<!-- svelte-ignore a11y-autofocus -->
<input
{disabled}
type="text"

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { createPopperActions } from 'svelte-popperjs'
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
import type { PopoverPlacement } from './Popover.model'
export let placement: PopoverPlacement = 'auto'
@@ -9,28 +9,16 @@
export let disapperTimoout = 100
const [popperRef, popperContent] = createPopperActions({ placement })
const betterPreventOverflow = (options) => ({
name: 'preventOverflow',
options,
effect: ({ state }) => {
const { padding = 0 } = options
state.elements.popper.style.maxWidth = `calc(100vw - ${padding * 2}px)`
}
})
const extraOpts = {
const popperOptions: PopperOptions<{}> = {
placement: 'bottom-end',
strategy: 'fixed',
modifiers: [
betterPreventOverflow({ padding: 10 }),
{
name: 'offset',
options: {
offset: [8, 8]
}
},
{ name: 'offset', options: { offset: [8, 8] } },
{
name: 'arrow',
options: {
padding: 10 // 5px from the edges of the popper
padding: 10
}
}
]
@@ -59,10 +47,10 @@
{/if}
{#if showTooltip && !disablePopup}
<div
use:popperContent={extraOpts}
use:popperContent={popperOptions}
on:mouseenter={open}
on:mouseleave={close}
class="z-50 py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800
class="z-50 py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800
whitespace-normal text-left {popupClass}"
>
<div class="max-w-sm">

View File

@@ -51,10 +51,6 @@
onDragStart(e)
}
function onHover(e) {
thumbHover = thumbHover ? false : true
}
function onDragStart(e) {
// If mouse event add a pointer events shield
if (e.type === 'mousedown') document.body.append(mouseEventShield)
@@ -182,6 +178,7 @@
>
<div class="range__track" bind:this={container}>
<div class="range__track--highlighted" bind:this={progressBar} />
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<div
class="range__thumb"
class:range__thumb--holding={holding}

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import { type Resource, ResourceService, type ResourceType, VariableService } from '$lib/gen'
import type { Schema } from '$lib/common'
import { ResourceService, type Resource } from '$lib/gen'
import { canWrite, emptyString, sendUserToast } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import type { Schema } from '$lib/common'
import { Alert, Button, Drawer, Skeleton } from './common'
import Path from './Path.svelte'
import Required from './Required.svelte'
import { Alert, Button, Drawer, Skeleton } from './common'
import { userStore, workspaceStore } from '$lib/stores'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import autosize from 'svelte-autosize'
import SimpleEditor from './SimpleEditor.svelte'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import autosize from 'svelte-autosize'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import SchemaForm from './SchemaForm.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
let path = ''
@@ -131,7 +131,7 @@
on:close={drawer.closeDrawer}
>
<div>
<div class="flex flex-col gap-3 py-3 text-gray-700">
<div class="flex flex-col gap-3 py-3 text-gray-700">
<div>
{#if !can_write}
<div class="m-2">

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import { ResourceService, type Resource } from '$lib/gen'
import { ResourceService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { faPen, faPlus, faRotateRight } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { Button } from './common'
import Select from 'svelte-select'
import AppConnect from './AppConnect.svelte'
import ResourceEditor from './ResourceEditor.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../defaults'
import AppConnect from './AppConnect.svelte'
import { Button } from './common'
import ResourceEditor from './ResourceEditor.svelte'
const dispatch = createEventDispatcher()

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import { page } from '$app/stores'
import {
decodeArgs,
defaultIfEmptyString,
displayDaysAgo,
emptyString,
@@ -10,16 +8,16 @@
} from '$lib/utils'
import { slide } from 'svelte/transition'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import SchemaForm from './SchemaForm.svelte'
import type { Schema } from '$lib/common'
import { runFormStore, userStore } from '$lib/stores'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import CliHelpBox from './CliHelpBox.svelte'
import { Badge, Button } from './common'
import InlineCodeCopy from './InlineCodeCopy.svelte'
import SchemaForm from './SchemaForm.svelte'
import SharedBadge from './SharedBadge.svelte'
import Toggle from './Toggle.svelte'
import { runFormStore, userStore } from '$lib/stores'
import Tooltip from './Tooltip.svelte'
import CliHelpBox from './CliHelpBox.svelte'
import InlineCodeCopy from './InlineCodeCopy.svelte'
export let runnable:
| {

View File

@@ -7,26 +7,26 @@
</script>
<script lang="ts">
import { sendUserToast, formatCron, canWrite, emptyString } from '$lib/utils'
import { ScriptService, Script, ScheduleService, type Flow, FlowService } from '$lib/gen'
import Toggle from '$lib/components/Toggle.svelte'
import Path from '$lib/components/Path.svelte'
import { Alert, Badge, Button } from '$lib/components/common'
import Tooltip from '$lib/components/Tooltip.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import Required from '$lib/components/Required.svelte'
import CronInput, { OFFSET } from '$lib/components/CronInput.svelte'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import { Alert, Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import CronInput from '$lib/components/CronInput.svelte'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { FlowService, ScheduleService, Script, ScriptService, type Flow } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { canWrite, emptyString, formatCron, sendUserToast } from '$lib/utils'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
let initialPath = ''
let edit = true
let schedule: string = '0 0 12 * *'
let timezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone
let itemKind: 'flow' | 'script' = 'script'
@@ -37,7 +37,11 @@
is_flow = isFlow
initialPath = ePath
itemKind = is_flow ? 'flow' : 'script'
path = ePath
if (path == ePath) {
loadSchedule()
} else {
path = ePath
}
edit = true
drawer?.openDrawer()
}
@@ -49,6 +53,7 @@
path = initialScriptPath
initialPath = initialScriptPath
script_path = initialScriptPath
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
drawer?.openDrawer()
}
@@ -91,6 +96,7 @@
})
enabled = s.enabled
schedule = s.schedule
timezone = s.timezone
script_path = s.script_path ?? ''
is_flow = s.is_flow
args = s.args ?? {}
@@ -107,6 +113,7 @@
path: initialPath,
requestBody: {
schedule: formatCron(schedule),
timezone,
args
}
})
@@ -117,7 +124,7 @@
requestBody: {
path,
schedule: formatCron(schedule),
offset: OFFSET,
timezone,
script_path,
is_flow,
args,
@@ -190,7 +197,8 @@
<span class="mr-1">Schedule</span>
<Tooltip>Schedules use CRON syntax. Seconds are mandatory.</Tooltip>
</h2>
<CronInput disabled={!can_write} bind:schedule bind:validCRON />
<CronInput disabled={!can_write} bind:schedule bind:timezone bind:validCRON />
<h2 class="border-b pb-1 mt-8 mb-2">Runnable</h2>
{#if !edit}

View File

@@ -2,7 +2,7 @@
import type { SchemaProperty } from '$lib/common'
export const ARG_TYPES = ['integer', 'number', 'string', 'boolean', 'object', 'array'] as const
export type ArgType = typeof ARG_TYPES[number]
export type ArgType = (typeof ARG_TYPES)[number]
export interface ModalSchemaProperty {
selectedType?: string
@@ -115,6 +115,7 @@
Name
<Required required={true} />
</div>
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
autocomplete="off"

View File

@@ -9,15 +9,16 @@
import Path from './Path.svelte'
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import CenteredPage from './CenteredPage.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
import { faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
import { Badge, Button, Drawer } from './common'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
import Tooltip from './Tooltip.svelte'
import SettingSection from './SettingSection.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { Pen } from 'lucide-svelte'
import autosize from 'svelte-autosize'
import type Editor from './Editor.svelte'
export let script: Script
export let initialPath: string = ''
@@ -26,6 +27,16 @@
export let lockedLanguage = false
export let topHash: string | undefined = undefined
let metadataOpen = initialPath == '' && $page.url.searchParams.get('state') == undefined
let advancedOpen = false
let editor: Editor | undefined = undefined
let scriptEditor: ScriptEditor | undefined = undefined
export function setCode(code: string): void {
editor?.setCode(code)
}
const langs: [string, SupportedLanguage][] = [
['Typescript', Script.language.DENO],
['Python', Script.language.PYTHON3],
@@ -55,12 +66,9 @@
]
let pathError = ''
let summaryC: HTMLInputElement | undefined = undefined
let pathC: Path | undefined = undefined
let loadingSave = false
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
$: step = Number($page.url.searchParams.get('step')) || 1
if (script.content == '') {
initContent(script.language, script.kind, template)
@@ -72,6 +80,7 @@
template: 'pgsql' | 'mysql' | 'script'
) {
script.content = initialCode(language, kind, template)
scriptEditor?.inferSchema(script.content, language)
}
async function editScript(leave: boolean): Promise<void> {
@@ -104,10 +113,10 @@
}
})
if (leave) {
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
history.replaceState(history.state, '', `/scripts/edit/${newHash}`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
} else {
await goto(`/scripts/edit/${newHash}?step=2`)
await goto(`/scripts/edit/${newHash}`)
script.hash = newHash
topHash = undefined
}
@@ -117,243 +126,231 @@
loadingSave = false
}
async function changeStep(step: number) {
if (step > 1) {
script.schema = script.schema ?? emptySchema()
try {
await inferArgs(script.language, script.content, script.schema)
} catch (error) {
console.info(
'The main signature was not parsable. This script is considered to be without main function'
)
const dropdownItems: Array<{ label: string; onClick: () => void }> = [
{
label: 'Save and leave',
onClick: () => editScript(true)
}
]
if (initialPath != '') {
dropdownItems.push({
label: 'Fork',
onClick: () => {
window.open(`/scripts/add?template=${initialPath}`)
}
}
goto(`?step=${step}`)
}
$: kind = script.kind as 'script' | 'trigger' | 'approval' | undefined
function onKeyDown(event: KeyboardEvent) {
if (event.key == 'Enter' && step == 1) {
changeStep(2)
}
})
}
</script>
{#if !$userStore?.operator}
<div class="flex flex-col h-screen">
<!-- Nav between steps-->
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
<div
class="justify-between flex flex-row w-full items-center overflow-x-auto scrollbar-hidden px-2"
>
<div class="flex flex-row py-1">
<Breadcrumb
items={['Metadata', 'Code', 'Advanced']}
selectedIndex={step}
on:select={(e) => changeStep(e.detail.index + 1)}
disabled={pathError != ''}
>
<svelte:fragment slot="separator">/</svelte:fragment>
</Breadcrumb>
</div>
<Drawer placement="right" bind:open={metadataOpen} size="800px">
<DrawerContent title="Metadata" on:close={() => (metadataOpen = false)}>
<h2 class="border-b pb-1 mb-4">Path</h2>
<Path
bind:error={pathError}
bind:path={script.path}
{initialPath}
namePlaceholder="script"
kind="script"
/>
<h2 class="border-b pb-1 mt-10 mb-4">Summary</h2>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
await changeStep(1)
setTimeout(() => pathC?.focus(), 100)
}}
>
{script.path}
</Button>
<input
type="text"
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
/>
<h2 class="border-b pb-1 mt-10 mb-4">Language</h2>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
await changeStep(1)
setTimeout(() => summaryC?.focus(), 100)
}}
>
<div class="max-w-[10em] !truncate">
{script.summary == '' || !script.summary ? 'No summary' : script.summary}
</div>
</Button>
{#if lockedLanguage}
<div class="text-sm text-gray-600 italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
<div class="flex flex-row gap-x-2">
{/if}
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Button
size="sm"
variant="border"
size="sm"
btnClasses={step == 1 ? 'hidden sm:invisible' : ''}
on:click={() => changeStep(step - 1)}
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
template = 'script'
initContent(lang, script.kind, template)
script.language = lang
}}
disabled={lockedLanguage}
>
Back
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={lockedLanguage}
on:click={() => {
template = 'pgsql'
initContent(Script.language.DENO, script.kind, template)
script.language = Script.language.DENO
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
</Button>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
<h2 class="border-b pb-1 mt-10 mb-4">Description</h2>
<textarea
use:autosize
bind:value={script.description}
placeholder="Edit description"
class="text-sm"
/>
</DrawerContent>
</Drawer>
<div class="flex flex-col h-screen">
<div class="flex flex-col w-full px-2 py-1 border-b shadow-sm">
<div class="justify-between flex gap-8 w-full items-center px-2">
<div class="min-w-64 w-full max-w-md">
<input
type="text"
placeholder="Script summary"
class="text-sm w-full font-semibold"
bind:value={script.summary}
/>
</div>
<div class="gap-4 flex">
<div class="flex justify-start w-full">
<div>
<button
on:click={async () => {
metadataOpen = true
}}
>
<Badge
color="gray"
class="center-center !bg-gray-300 !text-gray-600 !h-[28px] !w-[70px] rounded-r-none"
>
<Pen size={12} class="mr-2" /> Path
</Badge>
</button>
</div>
<input
type="text"
readonly
value={script.path}
size={script.path?.length || 50}
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none"
on:focus={({ currentTarget }) => {
currentTarget.select()
}}
/>
</div>
</div>
<div class="center-center">
<button
on:click={async () => {
metadataOpen = true
}}
>
<LanguageIcon lang={script.language} />
</button>
</div>
<div class="flex flex-row gap-x-4">
<Button
color="dark"
variant="border"
size="xs"
on:click={() => {
advancedOpen = true
}}
>
Customise
</Button>
<Button
size="sm"
variant={step == 1 ? 'contained' : 'border'}
btnClasses={step == 3 ? 'invisible' : 'inline-flex gap-2'}
disabled={step === 1 && pathError !== ''}
on:click={() => changeStep(step + 1)}
>
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
</Button>
<ButtonPopup
color="dark"
loading={loadingSave}
size="sm"
variant={step == 1 ? 'border' : 'contained'}
disabled={step === 1 && pathError !== ''}
startIcon={{ icon: faSave }}
on:click={() => editScript(false)}
{dropdownItems}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => editScript(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/scripts/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
Save
</Button>
</div>
</div>
</div>
<!-- metadata -->
{#if step === 1}
<CenteredPage>
<SettingSection title="Path" element="h2">
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="script"
kind="script"
/>
</SettingSection>
<SettingSection title="Summary" element="h2">
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
/>
</SettingSection>
<SettingSection title="Language" element="h2">
{#if lockedLanguage}
<div class="text-sm text-gray-600 italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
disabled={lockedLanguage}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
</Button>
{/each}
<Drawer bind:open={advancedOpen} size="800px">
<DrawerContent title="Customise" on:close={() => (advancedOpen = false)}>
<h2 class="border-b pb-1 mb-4"
>Script Kind &nbsp;<Tooltip
>Tag this script's purpose within flows such that it is available as the corresponding
action.</Tooltip
></h2
>
<div class="flex flex-wrap gap-2">
{#each scriptKindOptions as { value, title, desc }}
{@const isPicked = script.kind === value}
<Button
size="sm"
variant="border"
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={lockedLanguage}
color={isPicked ? 'blue' : 'dark'}
btnClasses="font-medium {isPicked ? '!bg-blue-50/75' : ''}"
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
template = 'script'
script.kind = value
initContent(script.language, value, template)
setCode(script.content)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
{title}
{#if desc}
<Tooltip class="mb-0.5 ml-1">
{desc}
</Tooltip>
{/if}
</Button>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
</SettingSection>
<SettingSection
title="Script kind"
element="h3"
tooltip="Tag this script as having a specific purpose inside flows. If it won't be used in flows,
you don't have to worry about this."
accordion
{/each}
</div>
<h2 class="border-b pb-1 mt-10 mb-4"
>Arguments &nbsp;<Tooltip
>The arguments are synced with the main signature but you may refine the parts that
cannot be inferred from the type directly.</Tooltip
></h2
>
<div class="flex flex-wrap gap-2">
{#each scriptKindOptions as { value, title, desc }}
{@const isPicked = script.kind === value}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses="font-medium {isPicked ? '!bg-blue-50/75' : ''}"
on:click={() => {
template = 'script'
script.kind = value
initContent(script.language, value, template)
}}
disabled={lockedLanguage}
>
{title}
{#if desc}
<Tooltip class="mb-0.5 ml-1">
{desc}
</Tooltip>
{/if}
</Button>
{/each}
</div>
</SettingSection>
</CenteredPage>
{:else if step === 2}
<ScriptEditor
bind:schema={script.schema}
path={script.path}
bind:code={script.content}
lang={script.language}
{initialArgs}
{kind}
/>
{:else if step === 3}
<CenteredPage>
<ScriptSchema bind:description={script.description} bind:schema={script.schema} />
</CenteredPage>
{/if}
<ScriptSchema bind:schema={script.schema} />
</DrawerContent>
</Drawer>
<ScriptEditor
bind:editor
bind:this={scriptEditor}
bind:schema={script.schema}
path={script.path}
bind:code={script.content}
lang={script.language}
{initialArgs}
kind={script.kind}
/>
</div>
{:else}
Script Builder not available to operators
{/if}
<svelte:window on:keydown={onKeyDown} />

View File

@@ -23,18 +23,16 @@
export let code: string
export let path: string | undefined
export let lang: Preview.language
export let kind: 'script' | 'trigger' | 'approval' | undefined = undefined
export let kind: string | undefined = undefined
export let initialArgs: Record<string, any> = {}
export let fixedOverflowWidgets = true
export let noSyncFromGithub = false
export let editor: Editor | undefined = undefined
let websocketAlive = { pyright: false, black: false, deno: false, go: false }
let width = 1200
// Internal state
let editor: Editor
let testJobLoader: TestJobLoader
// Test args input
@@ -70,7 +68,7 @@
})
}
async function inferSchema(code: string) {
export async function inferSchema(code: string, nlang?: 'go' | 'bash' | 'python3' | 'deno') {
schema = schema ?? emptySchema()
let isDefault: string[] = []
Object.entries(args).forEach(([k, v]) => {
@@ -80,10 +78,9 @@
})
try {
await inferArgs(lang, code, schema)
await inferArgs(nlang ?? lang, code, schema)
validCode = true
} catch (e) {
console.error("Couldn't infer args", e)
validCode = false
}
@@ -103,6 +100,10 @@
inferSchema(code)
loadPastTests()
})
function asKind(str: string | undefined) {
return str as 'script' | 'approval' | 'trigger' | undefined
}
</script>
<TestJobLoader
@@ -122,7 +123,7 @@
{editor}
{lang}
{websocketAlive}
{kind}
kind={asKind(kind)}
/>
{#if !noSyncFromGithub}
<div class="py-1">
@@ -145,37 +146,35 @@
<SplitPanesWrapper>
<Splitpanes class="!overflow-visible">
<Pane size={60} minSize={10} class="!overflow-visible">
<div
class="pl-2 h-full !overflow-visible"
on:mouseleave={() => {
inferSchema(code)
}}
>
<Editor
bind:code
bind:websocketAlive
bind:this={editor}
on:change={(e) => {
inferSchema(e.detail)
}}
cmdEnterAction={async () => {
await inferSchema(code)
runTest()
}}
formatAction={async () => {
await inferSchema(code)
try {
localStorage.setItem(path ?? 'last_save', code)
} catch (e) {
console.error('Could not save last_save to local storage', e)
}
lastSave = code
}}
class="flex flex-1 h-full !overflow-visible"
lang={scriptLangToEditorLang(lang)}
automaticLayout={true}
{fixedOverflowWidgets}
/>
<div class="pl-2 h-full !overflow-visible">
{#key lang}
<Editor
{path}
bind:code
bind:websocketAlive
bind:this={editor}
on:change={(e) => {
inferSchema(e.detail)
}}
cmdEnterAction={async () => {
await inferSchema(code)
runTest()
}}
formatAction={async () => {
await inferSchema(code)
try {
localStorage.setItem(path ?? 'last_save', code)
} catch (e) {
console.error('Could not save last_save to local storage', e)
}
lastSave = code
}}
class="flex flex-1 h-full !overflow-visible"
lang={scriptLangToEditorLang(lang)}
automaticLayout={true}
{fixedOverflowWidgets}
/>
{/key}
</div>
</Pane>
<Pane size={40} minSize={10}>
@@ -185,9 +184,10 @@
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
<WindmillIcon
white={true}
class="animate-[spin_5s_linear_infinite] mr-2 text-white"
class="mr-2 text-white"
height="20px"
width="20px"
spin="fast"
/>
Cancel
</Button>

View File

@@ -8,11 +8,8 @@
import Highlight from 'svelte-highlight'
import json from 'svelte-highlight/languages/json'
import { Alert } from './common'
import autosize from 'svelte-autosize'
export let schema: Schema
export let description: string | undefined
export function setSchema(newSchema: Schema) {
schema = newSchema
@@ -20,28 +17,12 @@
</script>
<div class="w-full">
<h1 class="my-4">Advanced</h1>
<Tabs selected="ui">
<Tab value="ui">Description & Arguments refinement</Tab>
<Tab value="ui">Arguments</Tab>
<Tab value="jsonschema">JSON Schema</Tab>
<svelte:fragment slot="content">
<TabContent value="ui">
<h2 class="border-b pb-1 mt-6 mb-4">Description</h2>
<textarea
use:autosize
bind:value={description}
placeholder="Edit description"
class="text-sm"
/>
<h2 class="border-b pb-1 mb-4 mt-8">Arguments</h2>
<Alert type="info" title="Synchronized with main signature">
Argument names, being required or not, and default values are derived from the main
signature of step 2 and cannot be edited directly. Change the main signature to edit them.
</Alert>
<div class="mt-4" />
<SchemaForm {schema} editableSchema={true} />
</TabContent>
<TabContent value="jsonschema">

View File

@@ -18,7 +18,7 @@
return
}
// pre-filter
let idxs = uf.filter(plaintextItems, filter)
let idxs = uf.filter(plaintextItems, filter) ?? []
let info = uf.info(idxs, plaintextItems, filter)
let order = uf.sort(info, plaintextItems, filter)

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